Tag

nlp Archives - Análise Macro

Text mining dos comunicados do FOMC: prevendo mudanças na política

By | Data Science

Neste exercício utilizaremos os statements do FOMC/FED para construir um índice de sentimentos, que busca quantificar sentimentos (i.e. positivo, negativo) através de informações textuais extraídas por técnicas de text mining. Em seguida, comparamos o índice com o principal instrumento de política monetária, a taxa de juros, e avaliamos sua utilidade em prever mudanças de política através do teste de causalidade de Granger.

Text mining

Se textos pudessem falar, o que eles diriam? Essa é a ideia central por trás das técnicas de text mining existentes, que buscam extrair insights de dados textuais (i.e. notícias, atas, comunicados, livros, etc.). Quando lemos um texto, usamos naturalmente nossa compreensão individual sobre o sentido emocional das palavras para inferir o sentimento/emoção de um trecho do texto (risco = negativo, ganho = positivo). Dessa forma, as técnicas de text mining se propõem a realizar essa mesma "inferência" de maneira quantitativa. Em outras palavras, nós escrevemos um código que "lê" um texto e informa em números, com base em classificações prévias ou por modelos, o sentimento do mesmo.

A técnica de text mining mais simples é a que contabiliza o número de palavras positivas/negativas em um dado texto e, a partir disso, calcula um índice de sentimentos como:

Sentimento = nº positivas - nº negativas

Valores positivos indicam sentimos positivos, valores negativos indicam sentimentos negativos e valor igual a zero indica neutralidade. Essa não é a única técnica de analisar sentimentos de um texto, mas é comumente a mais utilizada. Mas como definir se uma palavra de um texto é positiva ou negativa?

A análise de sentimento é uma grande área de pesquisa - muitas vezes vista como uma subárea do Processamento de Linguagem Natural (NLP, no inglês) - que, ao longo dos anos, vem possibilitando a pesquisadores e profissionais de mercado o uso de dicionários de sentimento, também chamados de "léxicos", que classificam um grande conjunto de palavras comumente usadas em documentos em positivas/negativas.

Exemplos de dicionários/léxicos para análise de sentimentos:

  • Loughran-McDonald sentiment lexicon (muito usado em textos de finanças);
  • Bing sentiment lexicon (de uso geral);
  • AFINN-111 dataset (mede o sentimento em uma escala de -5 a 5)

Veja as referências para mais detalhes.

Mas na prática, como esses dicionários se parecem? Abaixo apresentamos 12 palavras aleatórias (de um total de 4.150) do dicionário Loughran-McDonald e a classificação de sentimento correspondente:

Observe que o dicionário apresenta 7 possíveis classificações de sentimentos para as palavras: "negative", "positive", "uncertainty", "litigious", "constraining" e "superfluous". Entretanto, na prática o mais comum e simples é analisar o sentimento de um texto no "modo binário", ou seja, usando apenas as classificações de positivo/negativo. Portanto, como podemos utilizar este dicionário de maneira prática?

Em resumo, uma análise de sentimentos pode ser desenvolvida em 4 etapas:

  • Dados textuais: definir quais dados textuais serão alvo da análise, assim como identificar sua localização (online);
  • Pré-processamento: são rotinas de coleta e tratamento dos dados textuais, o que envolve a aplicação de técnicas de web scraping (veja um tutorial aqui), de tokenização (veja um tutorial aqui) e de manipulação de strings em geral;
  • Classificação de Sentimentos: cruza os dados textuais tratados, já representados na unidade (token) de palavras, com um dicionário/léxico de sentimentos e contabiliza o nº palavras positivas/negativas no texto;
  • Índice de Sentimentos: calcula o índice conforme a expressão matemática apresentada acima.

Os procedimentos expostos não exigem o uso de nenhum método de estimação estatística, apesar de haverem outras técnicas de text mining para tal, portanto são acessíveis a maioria das pessoas. A parte mais difícil para iniciantes são as etapas de coleta de dados (web scraping), principalmente se o conjunto de textos alvo da análise é grande em volume e/ou quantidade. Nestes casos, procedimentos manuais de coleta de dados são inviáveis e você precisa sair da zona de conforto da sua área de pesquisa/trabalho e entender alguns jargões das linguagens de desenvolvimento web. A boa notícia é que usando o R não precisamos necessariamente entender sobre essas outras linguagens, dado que existem pacotes/funções que fazem o trabalho por nós, em uma sintaxe quase que natural de se ler.

A seguir apresentamos algumas informações úteis sobre o procedimento de análise de sentimentos aplicada aos comunicados do FOMC/FED. Todo o código necessário para replicação está disponibilizado ao final deste material.

Web scraping

Em geral, as informações textuais estão disponíveis digitalmente, hospedadas em algum site na internet. Como exemplo deste exercício, utilizamos o site do FOMC/FED, que disponibiliza, dentre outros, os documentos referentes aos statements desde 1936. Estes documentos são divulgados logo após cada reunião da autoridade monetária e servem para comunicar sua decisão referente a taxa de juros da economia (FED Funds Target Rate).

Como exemplo, na imagem a seguir trazemos o primeiro statement que iremos utilizar no exercício:

No total, utilizamos uma amostra de 205 statements no período 1998 até 2022. O procedimento de web scraping destes documentos no site do FED pode ser desenvolvido de maneira automatizada usando o R, conforme o código ao final (para mais informações veja este tutorial).

Índice de Sentimentos

Na etapa de pré-processamento removemos as stop words comuns da língua inglesa ("the", "of", "in", etc.), pontuações e números. A tokenização é feita a nível de palavra por cada statement e, desse resultado, cruzamos os dados com a tabela do dicionário de Loughran-McDonald para contabilizar as palavras positivas/negativas. Por fim, o índice é calculado, conforme apresentado acima, e visualizações de dados como essa abaixo podem ser geradas:

No gráfico acima comparamos os movimentos da média móvel de 8 períodos do índice de sentimentos calculado, por ser uma série menos ruidosa, com a taxa de juros da economia norte-americana, ou seja, com os ciclos de expansão ou contração monetária. Podemos observar que o índice de sentimentos parece, em certa medida, "antecipar" os movimentos de política monetária. O período mais notório é o da crise financeira de 2008 (Grande Recessão), no qual o FED fez uso do instrumento denominado quantitative easing (QE) e reduziu a taxa de juros para o intervalo mínimo de 0% (Zero Lower Bound ou ZLB), enquanto que os sentimentos captados nos statements anteriores ao "estouro" da crise eram continuamente decrescentes, em sentido negativo.

Outra característica interessante que o índice de sentimentos traz é seu estado conforme os mandatos do FED. Nota-se que no último mandato (Yellen) e no atual (Powell) o índice não esteve uma única vez negativo, para qualquer dos statements analisados no período. Isso não significa que o sentimento não tenha variado no período. Por exemplo, observe que no mandato da Yellen o índice começa próximo ao pico do mandato anterior, atinge o pico da série histórica e cai até abaixo do seu início.

Note ainda que mesmo durante períodos de política de ZLB o índice capta a variação dos sentimentos expressos nos comunicados. Como exemplo recente, com a pressão inflacionária e o aumento da taxa de juros - e consequente saída do ZLB - os sentimentos estão se aproximando novamente da região negativa.

A questão que fica é: este humilde índice de sentimentos é capaz de antecipar as mudanças de política monetária?

Causalidade de Granger

De forma a testar estatisticamente se a interpretação gráfica que fiz acima faz algum sentido, agora eu aplico um teste de causalidade de Granger. O teste serve para verificar se uma série temporal é útil em termos de previsão de outra série temporal, ou seja, o termo "causalidade" deve ser pensado como "precedência temporal".

Intuitivamente, com a aplicação do teste podemos dizer que uma série temporal X Granger-causa outra série temporal Y se as previsões dos valores de Y com base em seus próprios valores defasados e nos valores defasados de X forem "melhores" do que as previsões de Y baseadas apenas nos próprios valores defasados de Y. Formalmente, o teste compara dois modelos de regressão linear:

  • Modelo restritoyt = α0 + α1yt−1 + ⋯ + αmyt−m + errot
  • Modelo irrestritoyt = α0 + α1yt−1 + ⋯ + αmyt−m + β1xt-1 + ⋯ + βmxt−m + errot

O número de defasagens m deve ser especificado pelo usuário. Dessa forma, testamos as hipóteses:

  • Hipótese nula: a série temporal X não Granger-causa a série Y
  • Hipótese alternativa: a série temporal X Granger-causa a série Y

Um teste F é então aplicado para determinar se os coeficientes dos valores passados de xt são conjuntamente iguais a zero (H0: β1 = ⋯ = βm = 0), produzindo um p-valor. Dessa forma, se o p-valor for menor que um determinado nível de significância (por exemplo, α = 0,05), podemos rejeitar a hipótese nula e dizer que a série temporal X Granger-causa a série Y. Isso significa que os valores de previstos para Y baseados em suas próprias defasagens e em defasagens de X são melhores do que valores previstos para Y baseados somente em defasagens de Y. Usualmente aplica-se o teste em ambas as direções, ou seja, de X Y e de Y X.

Aplicando o teste de causalidade de Granger sobre as séries do Índice de Sentimentos (IS, em nível, não a média móvel) e do FED Funds Rate (FFR, na primeira diferença), em ambas as direções, temos como resultado:

Analisando os resultados, na direção IS → FFR rejeitamos a hipótese nula ao nível de 5%, ou seja, podemos dizer que há precedência temporal ou, mais formalmente, a série temporal IS Granger-causa a série temporal FFR. Na direção oposta falha-se em rejeita a hipótese nula. Acredito que minha análise visual de antes está correta.

Código de replicação (R)

O código completo para replicação do exercício está disponível no Clube AM. Note que o procedimento de web scraping no site do FED está automatizado, não havendo garantias de funcionamento se houverem mudanças no mesmo. Além disso, você pode obter resultados diferentes dos aqui expostos conforme a amostra de textos extraídos do site for crescendo ao longo do tempo.

Para entender mais sobre os procedimentos deste exercício, você também pode tirar dúvidas com a nossa equipe dentro do Clube AM.

Referências

Granger, C. W. (1969). Investigating causal relations by econometric models and cross-spectral methods. Econometrica: journal of the Econometric Society, 424-438.

Hu, M., & Liu, B. (2004). Mining and summarizing customer reviews. In Proceedings of the tenth ACM SIGKDD international conference on Knowledge discovery and data mining (pp. 168-177).

Loughran, T., & McDonald, B. (2011). When is a liability not a liability? Textual analysis, dictionaries, and 10‐Ks. The Journal of finance, 66(1), 35-65.

Nielsen, F. Å. (2011). A new ANEW: Evaluation of a word list for sentiment analysis in microblogs. arXiv preprint arXiv:1103.2903.

Silge, J., & Robinson, D. (2017). Text mining with R: A tidy approach. O'Reilly Media, Inc.

Shapiro, A. H., & Wilson, D. (2021). Taking the Fed at its word: A new approach to estimating central bank objectives using text analysis. Federal Reserve Bank of San Francisco.

Wu, J. C., & Xia, F. D. (2016). Measuring the macroeconomic impact of monetary policy at the zero lower bound. Journal of Money, Credit and Banking, 48(2-3), 253-291.

 

Topic Modeling: sobre o que o COPOM está discutindo? Uma aplicação do modelo LDA

By | Data Science

O que informações textuais podem revelar sobre a situação da economia? Como transformar palavras em estatísticas e obter insights? Há algo informativo nas entrelinhas das atas do COPOM? Como usar Machine Learning para interpretar os comunicados da autoridade monetária? Neste exercício, damos continuidade aos posts sobre Natural Language Processing (NLP) demonstrando a aplicação da técnica de topic modeling com as atas do COPOM.

O que é topic modeling?

No contexto de text mining, nossos dados geralmente são coleções de documentos textuais, como postagens em blogs ou artigos de notícias, que gostaríamos de dividir em grupos para que possamos entendê-los separadamente. A modelagem de tópicos (topic modeling) é um método de Machine Learning para classificação não supervisionada de tais documentos, semelhante ao agrupamento em dados numéricos, que encontra grupos de observações mesmo quando não temos certeza do que estamos procurando. Em outras palavras, é um método para classificar/mapear textos em um número k de tópicos.

O modelo LDA

O modelo Latent Dirichlet Allocation (LDA) é um método probabilístico particularmente popular para estimar um modelo de tópico. Sem mergulhar na matemática por trás do modelo, podemos entendê-lo como sendo guiado por dois princípios:

  • Cada documento textual é uma mistura de tópicos: imagine que cada documento pode conter palavras de vários tópicos em proporções particulares. Por exemplo, em um modelo de dois tópicos, poderíamos dizer “Documento 1 é 90% tópico A e 10% tópico B, enquanto o Documento 2 é 30% tópico A e 70% tópico B”.
  • Cada tópico é uma mistura de palavras: por exemplo, poderíamos imaginar um modelo de dois tópicos para as notícias vinculadas na mídia brasileira, com um tópico para “política” e outro para “entretenimento”. As palavras mais comuns no tópico de política podem ser “presidente”, “congresso” e “governo”, enquanto que o tópico de entretenimento pode ser composto por palavras como “filmes”, “televisão” e “ator”. É importante ressaltar que as palavras podem ser compartilhadas entre os tópicos, ou seja, uma palavra como “dinheiro” pode aparecer em ambos os tópicos.

O modelo LDA estima as probabilidades de ambos ao mesmo tempo: encontrar a mistura de palavras que está associada a cada tópico, assim como também determinar a mistura de tópicos que descreve cada documento. Em nossa análise, um documento corresponde a cada texto que será extraído de cada ata do COPOM.

Existem várias implementações desse algoritmo, aqui exploraremos a abordagem que usa iterações de amostragem de Gibbs. Em resumo, construiremos esta análise:

Pacotes

Para reproduzir o exercício a seguir você precisará dos seguintes pacotes de R:


# Carregar pacotes/dependências
library(jsonlite) # CRAN v1.7.3
library(purrr) # CRAN v0.3.4
library(dplyr) # CRAN v1.0.8
library(pdftools) # CRAN v3.1.1
library(magrittr) # CRAN v2.0.2
library(stringr) # CRAN v1.4.0
library(tidyr) # CRAN v1.2.0
library(lubridate) # CRAN v1.8.0
library(forcats) # CRAN v0.5.1
library(ggplot2) # CRAN v3.3.5
library(ggtext) # CRAN v0.1.1
library(tsibble) # CRAN v1.1.1
library(scales) # CRAN v1.1.1
library(tm) # CRAN v0.7-8
library(tidytext) # CRAN v0.3.2
library(topicmodels) # CRAN v0.2-12

Os dados

Como insumo para o modelo, precisamos dos textos das atas do COPOM, disponibilizados em arquivos PDFs no site do BCB. Conforme procedimentos já detalhados em um post anterior (clique aqui), o código abaixo importa os textos brutos e os salva em um objeto tabular:


# URL para página com JSON dos links das atas do COPOM
url_atas <- paste0(
"https://www.bcb.gov.br/api/servico/sitebcb/copomminutes/",
"ultimas?quantidade=2000&filtro="
)
# Web scraping para importar texto dos PDFs das atas
raw_copom <- jsonlite::fromJSON(url_atas) %>%
purrr::pluck("conteudo") %>%
dplyr::as_tibble() %>%
dplyr::select(
"date" = "DataReferencia",
"meeting" = "Titulo",
"url" = "Url",
) %>%
dplyr::mutate(url = paste0("https://www.bcb.gov.br", url)) %>%
dplyr::mutate(text = purrr::map(URLencode(url), pdftools::pdf_text))

Pré-processamento

Os dados importados vêm recheados de possíveis problemas se fossem diretamente utilizados no modelo LDA. Desta forma, é necessário realizar alguns tratamentos, que se resumem a:

  • Determinar amostra de atas do COPOM de interesse (50ª ata/agosto de 2000 em diante);
  • Remover caracteres e códigos indesejados no meio das palavras;
  • Remover pontuações e números;
  • Remover as stops words (palavras sem valor semântico como "que", "de", "e", etc.);
  • Aplicar o processo de stemming: transformar várias palavras que significam a mesma coisa em apenas uma palavra. Por exemplo, palavras como “economia”, “economista” e “economizar” se tornam “economi”.

# Limpeza e pré-processamento dos textos das atas
copom_clean <- raw_copom %>%
dplyr::filter(
# Remover o que não é reunião do Copom e reuniões nº 42 a 49
!meeting == "Changes in Copom meetings",
!stringr::str_detect(string = meeting, pattern = "^4[2-9]")
) %>%
# Transforma cada elemento (página) do vetor textual das atas
# um linha no data.frame
tidyr::unnest(text) %>%
dplyr::group_by(meeting) %>%
dplyr::mutate(
# Trata end-of-line/carriage return dos textos
text = stringr::str_split(string = text, pattern = "\r") %>%
# Remove códigos ASCII/caracteres indesejados
stringr::str_replace_all(
pattern = "\n|\003|\017|\023|\024|\025|\026|\027|\028|\029|\030|\031|\032|\033|\034|\035",
replacement = ""
) %>%
# Remove pontuações e números
tm::removePunctuation() %>%
tm::removeNumbers() %>%
stringr::str_to_lower() %>%
# Remove "stop words"
tm::removeWords(words = tm::stopwords(kind = "english")) %>%
# Normalização de palavras: stemming
tm::stemDocument(),
# Transforma informação do nº XX da ata para padrão "Ata XX"
meeting = stringr::str_sub(string = meeting, start = 1, end = 3) %>%
stringr::str_remove(pattern = "[:alpha:]") %>%
stringr::str_c("Ata ", .),
# Transforma data para YYYY-MM-DD
date = lubridate::as_date(.data$date)
) %>%
dplyr::ungroup() %>%
dplyr::arrange(date)

Tokenização

Em seguida, precisamos obter a frequência de cada palavra em cada documento/ata, de modo a obter um objeto tabular onde cada linha contém um palavra por documento, conforme abaixo:


# Tokenização
copom_token <- copom_clean %>%
# Contabiliza frequência das palavras
tidytext::unnest_tokens(output = term, input = text, token = "words") %>%
dplyr::count(meeting, term, sort = TRUE, name = "count") %>%
dplyr::ungroup()

Representação matricial

Agora transformamos o objeto anterior para a representação matricial M por N que representará a frequência das palavras nas atas, onde as linhas  M representam as atas e as colunas N são as palavras, e os valores são as frequências das palavras. Além disso, aplicamos outro tratamento para remover palavras que aparecem com pouca frequência nas atas, processo similar a uma remoção de "outliers":


# Transformar em objeto DocumentTermMatrix
copom_dtm <- copom_token %>%
tidytext::cast_dtm(
document = meeting,
term = term,
value = count,
weighting = tm::weightTf
) %>%
# Remoção de "outliers": manter apenas termos que aparecem
# em 80% dos documentos (atas)
tm::removeSparseTerms(sparse = 0.2)

Estimar modelo LDA

Para estimar o modelo LDA usamos o pacote topicmodels, com os valores padrão dos argumentos fornecidos pela função LDA(), exceto para alguns controles do amostrador de Gibbs e para o argumento k, que indica o número de tópicos a serem usados para classificar as atas do COPOM.

A especificação do parâmetro k é relativamente arbitrária, e apesar de existirem abordagens analíticas para decidir o valor de k, a maior parte da literatura o define de forma ad hoc. Ao escolher k temos dois objetivos que estão em conflito direto entre si. Queremos "prever" corretamente o texto e ser o mais específico possível para determinar o número de tópicos. No entanto, ao mesmo tempo, queremos ser capazes de interpretar nossos resultados e, quando nos tornamos muito específicos, o significado geral de cada tópico se perde.

A seguir definimos k = 5, assumindo que os diretores do BCB discutem apenas 5 tópicos que culminam nas atas do COPOM (você pode testar outros valores de k e ajustar o valor conforme necessidade).


# Estimar modelo Latent Dirichlet Allocation (LDA) usando Gibbs Sampling
copom_lda <- topicmodels::LDA(
x = copom_dtm,
k = 5,
method = "Gibbs",
control = list(
# nº de simulações repetidas com inicialização aleatória
nstart = 5,
# semente para reprodução (tamanho tem que ser igual a nstart)
seed = as.list(1984:1988),
# nº iterações descartadas
burnin = 3000,
# nº de iterações
iter = 2000,
# nº de iterações retornadas
thin = 500
)
)

Resultados do modelo

Agora focamos na interpretação dos resultados do modelo estimado. Primeiro, podemos averiguar quais palavras foram associadas a cada tópico:


# Obter palavras associadas a cada tópico latente
topicmodels::terms(copom_lda, k = 10)

# Topic 1 Topic 2 Topic 3 Topic 4 Topic 5
# [1,] "price" "inflat" "increas" "price" "inflat"
# [2,] "rate" "increas" "month" "inflat" "copom"
# [3,] "increas" "price" "reach" "chang" "monetari"
# [4,] "exchang" "month" "accord" "consid" "polici"
# [5,] "result" "may" "data" "copom" "econom"
# [6,] "indic" "year" "product" "regard" "risk"
# [7,] "due" "rate" "respect" "economi" "scenario"
# [8,] "declin" "period" "good" "monetari" "economi"
# [9,] "month" "industri" "quarter" "growth" "committe"
# [10,] "polici" "market" "last" "project" "rate"

Além dos palavras, também podemos obter suas probabilidades - chamada de  ("beta") - associadas a cada tópico estimadas pelo modelo:


tidytext::tidy(copom_lda, matrix = "beta")

# # A tibble: 985 x 3
# topic term beta
# <int> <chr> <dbl>
# 1 1 increas 0.0368
# 2 2 increas 0.0485
# 3 3 increas 0.0818
# 4 4 increas 0.00000203
# 5 5 increas 0.0000876
# 6 1 month 0.0195
# 7 2 month 0.0387
# 8 3 month 0.0581
# 9 4 month 0.00000203
# 10 5 month 0.0000235
# # ... with 975 more rows

Note que, por exemplo, o termo "increas" tem probabilidade de 0,0368 de ter sido gerado do tópico 1, mas 0.0818 de probabilidade de ter sido gerado do tópico 3.

Podemos gerar uma visualização para verificar quais são os 10 termos mais "comuns" de cada tópico, conforme abaixo:


tidytext::tidy(copom_lda, matrix = "beta") %>%
dplyr::group_by(topic) %>%
dplyr::slice_max(beta, n = 10) %>%
dplyr::ungroup() %>%
dplyr::arrange(topic, -beta) %>%
dplyr::mutate(term = tidytext::reorder_within(term, beta, topic)) %>%
ggplot2::ggplot() +
ggplot2::aes(x = beta, y = term, fill = factor(topic)) +
ggplot2::geom_col(show.legend = FALSE) +
ggplot2::facet_wrap(~topic, scales = "free") +
tidytext::scale_y_reordered() +
ggplot2::labs(title = "Probabilidades por tópico por palavras")

A visualização nos permite entender mais facilmente sobre os 5 tópicos que foram classificados através das atas do COPOM. As palavras mais comuns no tópico 1 incluem "rate", "price" e "increas", o que sugere que podem representar a discussão da decisão da política monetária quanto a taxa de juros. No tópico 2 são associados termos referentes a dinâmica corrente da inflação, como "inflat", "price" e "increas". Já o tópico 3 cobre discussões sobre os dados quanto ao atual nível da atividade econômica do país, incluindo termos como "data", "product" e "reach". Por sua vez, no tópico 4 aparecem termos relacionados às expectativas de inflação com os termos "project", "inflat" e "price". Por fim, no tópico 5 predominam termos associados a própria ação da autoridade monetária, incluindo termos como "copom", "monetari", "polici" e "committe".

Uma observação importante sobre as palavras em cada tópico é que algumas palavras, como “price” e “inflat”, aparecem em mais de um tópico. Esta é uma vantagem da modelagem de tópicos em oposição aos métodos de “agrupamento rígido”: tópicos usados em linguagem natural podem ter alguma sobreposição em termos de palavras. Em contrapartida, como desvantagem, podem haver tópicos demasiadamente redundantes.

Por fim, vamos analisar as probabilidades por documento (ata) por tópico, de modo a verificar os tópicos "mais discutidos" em cada reunião do COPOM. Essas probabilidades são chamadas de  ("gamma") e podem ser obtidas conforme abaixo:


# Probabilidades de cada tópico por documento
copom_topics <- tidytext::tidy(copom_lda, matrix = "gamma") %>%
dplyr::left_join(
dplyr::distinct(copom_clean, meeting, date),
by = c("document" = "meeting")
) %>%
dplyr::arrange(date) %>%
dplyr::mutate(
date = as.character(date),
topic = dplyr::recode(
topic,
"1" = "Taxa de Juros",
"2" = "Inflação",
"3" = "Atividade Econômica",
"4" = "Expectativas de Inflação",
"5" = "Intervenção Monetária"
) %>%
forcats::as_factor()
)

copom_topics

# # A tibble: 975 x 4
# document topic gamma date
# <chr> <fct> <dbl> <chr>
# 1 Ata 50 Taxa de Juros 0.311 2000-09-12
# 2 Ata 50 Inflação 0.324 2000-09-12
# 3 Ata 50 Atividade Econômica 0.135 2000-09-12
# 4 Ata 50 Expectativas de Inflação 0.174 2000-09-12
# 5 Ata 50 Intervenção Monetária 0.0554 2000-09-12
# 6 Ata 51 Taxa de Juros 0.342 2000-10-04
# 7 Ata 51 Inflação 0.261 2000-10-04
# 8 Ata 51 Atividade Econômica 0.115 2000-10-04
# 9 Ata 51 Expectativas de Inflação 0.221 2000-10-04
# 10 Ata 51 Intervenção Monetária 0.0604 2000-10-04
# # ... with 965 more rows

Cada um desses valores é uma proporção estimada de palavras do documento que são geradas a partir do tópico. Por exemplo, o modelo estima que apenas cerca de 31% das palavras Ata nº 50 foram geradas a partir do tópico 1 (Taxa de Juros).

Com estes valores podem facilmente construir uma visualização para perceber a distribuição ao longo do tempo, conforme abaixo:


# Cores para gráfico
colors <- c(
blue = "#282f6b",
red = "#b22200",
yellow = "#eace3f",
green = "#224f20",
purple = "#5f487c",
black = "black"
)
# Função para formatar textos do eixo x (datas)
ym_label <- function(x) {
x <- lubridate::as_date(x)
dplyr::if_else(
is.na(dplyr::lag(x)) | tsibble::yearmonth(dplyr::lag(x)) != tsibble::yearmonth(x),
paste(lubridate::month(x, label = TRUE), "\n", lubridate::year(x)),
paste(lubridate::month(x, label = TRUE))
)
}
# Gráfico de colunas das probabilidades de cada tópico por documento
copom_topics %>%
ggplot2::ggplot() +
ggplot2::aes(x = date, y = gamma, fill = topic) +
ggplot2::geom_col() +
ggplot2::scale_x_discrete(
breaks = copom_topics %>%
dplyr::distinct(date) %>%
dplyr::filter(dplyr::row_number() %% 9 == 1) %>%
dplyr::pull(date),
labels = ym_label
) +
ggplot2::scale_y_continuous(
breaks = scales::extended_breaks(n = 6),
labels = scales::label_number(big.mark = ".", decimal.mark = ","),
expand = c(0, 0)
) +
ggplot2::scale_fill_manual(values = unname(colors)) +
ggplot2::labs(
title = "**Um conto de 4 crises**: o debate interno no COPOM",
subtitle = "Tópicos extraídos via modelo LDA usando textos das atas das reuniões",
y = "Probabilidade do Tópico",
x = "Data da Reunião",
fill = NULL,
caption = "**Dados**: BCB | **Elaboração**: analisemacro.com.br"
) +
ggplot2::theme_light() +
ggplot2::theme(
plot.title = ggtext::element_markdown(size = 28, colour = colors["blue"]),
plot.subtitle = ggtext::element_markdown(size = 14, face = "bold"),
plot.caption = ggtext::element_textbox_simple(size = 11),
legend.position = "top",
legend.text = ggplot2::element_text(size = 12, face = "bold"),
axis.text = ggplot2::element_text(size = 11, face = "bold"),
axis.title = ggtext::element_markdown(size = 12, face = "bold"),
axis.ticks = ggplot2::element_line(size = 0.7, color = "grey30"),
axis.ticks.length = ggplot2::unit(x = 1, units = "mm")
)

Podemos perceber que o modelo capta corretamente a "história" da política monetária brasileira ao longo destas duas últimas decadas. Em resumo, o choque de expectativas da eleição de 2002 (Lula) fez com que o BCB intervisse com juros, movimento visível no gráfico. Durante a crise financeira global de 2008, as discussões sobre o crescimento econômico cresceram significativamente. Já na crise da nova matriz econômica, durante os governos de Dilma Russef, as expectativas de inflação foram abruptamente se deteriorando e, com a mudança da diretoria do BCB em 2016, o aperto monetário tomou a cena. Por fim, e mais recentemente, preocupações com a escalada inflacionária e com as expectativas de inflação tiveram um aumento significativo da probabilidade de terem sido mais discutidas durante a pandemia da Covid-19.

Pessoalmente, estou satisfeito com os resultados encontrados. O modelo LDA parece ter se ajustado conforme o verdadeiro "tom" da política monetária no período. Porém, obviamente, como todo modelo há sempre espaços para aperfeiçoamentos e revisões, fique a vontade para usar o código disponível para tal.

Espero que o exercício tenha sido útil para você. Em última instância, a técnica aqui apresentada ao menos nos economiza o considerável tempo que seria necessário lendo todas as atas do COPOM para, só então, identificar tópicos discutidos.

Saiba mais

Posts sobre o assunto:

Referências:

  • Silge, J., & Robinson, D. (2017). Text Mining with R: A Tidy Approach (1st ed.). O’Reilly Media.
  • Benchimol, J., Kazinnik, S., & Saadon, Y. (2021). Federal Reserve communication and the COVID-19 pandemic. Covid Economics, 218.
  • Benchimol, J., Kazinnik, S., & Saadon, Y. (2020). Text mining methodologies with R: An application to central bank texts. Bank of Israel, Research Department.
  • Blei D. M., Ng A. Y., Jordan M. I. (2003). Latent Dirichlet Allocation. Journal of Machine Learning Research, 3, 993–1022.

Detecção de plágio com NLP no R: o caso Decotelli

By | Data Science

Natural Language Processing (NLP) é um tópico interessante que vem sendo bastante explorado no mundo da ciência de dados. Aqui no blog já exploramos, por exemplo, a mineração de textos (text mining) das atas do COPOM (códigos disponíveis em R). No texto de hoje exploraremos mais o assunto aplicado ao problema de detecção de plágio entre dois ou mais textos, usando como exemplo um evento repercutido no ano passado envolvendo o ex-Ministro da Educação, Carlos Decotelli.

Para contextualizar, e para quem não teve conhecimento do evento em questão, em junho de 2020 houveram apontamentos de que a dissertação de mestrado do então Ministro da Educação, Carlos Decotelli, continha indícios de plágio. Muitas notícias foram vinculadas na época e após o “escândalo” o Ministro entregou uma carta de demissão.

Dados os atores envolvidos e repercussão deste evento, é natural aos cientistas de dados se perguntarem se o plágio pode ser verificado de forma mais empírica. Neste texto faremos um exercício aplicado de R para tentar responder essa questão, usando o pacote textreuse.

O pacote textreuse serve muito bem o propósito deste exercício, pois fornece um conjunto de funções para medir a similaridade entre documentos de texto e detectar passagens que foram reutilizadas. Para realizar essa mensuração existem algumas possibilidades, entre elas o Índice de Jaccard - também conhecido como coeficiente de similaridade de Jaccard - que oferece uma estatística para medir a similaridade entre conjuntos amostrais, expresso conforme abaixo:

O Índice de Jaccard é muito simples quando queremos identificar similaridade entre textos, sendo os coeficientes expressos como números entre 0 e 1. Dessa forma, quanto maior o número, mais semelhantes são os textos (conjuntos amostrais).

Agora vamos à prática!

Pacotes

Para reproduzir os código de R deste exercício, certifique de que tenha os seguintes pacotes instalados/carregados:


# Carregar pacotes
library(textreuse) # CRAN v0.1.5
library(magrittr) # CRAN v2.0.1
library(dplyr) # CRAN v1.0.7
library(stringr) # CRAN v1.4.0
library(ggplot2) # CRAN v3.3.5
library(ggtext) # CRAN v0.1.1
library(scales) # CRAN v1.1.1

Dados

Os dados utilizados são os documentos que queremos comparar a similaridade. O principal documento é a dissertação de mestrado de Carlos Decotelli, e os demais são documentos indicados por Thomas Conti de onde os trechos teriam sido copiados. Nos links à seguir você pode baixar os originais de cada documento:

  1. Dissertação Carlos Decotelli (2008): https://bibliotecadigital.fgv.br/dspace/handle/10438/3726
  2. Relatório CVM (2007) - The Internet Archive: http://web.archive.org/web/20200629224030/https://ri.banrisul.com.br/banrisul/web/arquivos/Banrisul_DFP2007b_port.pdf
  3. “A Abordagem Institucional na Administração” de Alexandre Rosa e Cláudia Coser (2003): https://twiki.ufba.br/twiki/bin/viewfile/PROGESP/ItemAcervo241?rev=&filename=Abordagem_institucional_na_administracao.pdf
  4. “Mudança e estratégia nas organizações” de Clóvis L. Machado-da-Silva et al. (1999): http://anpad.org.br/admin/pdf/enanpad1998-orgest-26.pdf

Para usar esses documentos no pacote textreuse precisamos transformá-los para o formato de texto (extensão .txt). Existem formas de fazer isso por meio de pacotes no R, no entanto, para simplificar eu usei ferramentas online que fazem esse serviço, tomando o cuidado de remover as páginas pré ou pós textuais (capa, sumário, referências, etc.) antes da conversão, já que o objetivo é comparar os textos do corpo dos documentos propriamente ditos.

Ferramenta online para converter de PDF para TXT: https://pdftotext.com/pt/

Após esse tratamento/conversão teremos 4 arquivos de texto para realizar a análise. Disponibilizei os arquivos neste link para quem se interessar em reproduzir. Na sequência importamos os arquivos para o R:


# Criar pasta temporária
temp_folder <- tempdir()

# Baixar arquivos txt (zipados)
download.file(
url = "https://analisemacro.com.br/download/34322/",
destfile = paste0(temp_folder, "\\decotelli.zip"),
mode = "wb" # talvez você precise mudar esse argumento, dependendo do seu sistema
)

# Descompactar
unzip(
zipfile = paste0(temp_folder, "\\decotelli.zip"),
exdir = paste0(temp_folder, "txts")
)

# Listar arquivos na pasta
list.files(paste0(temp_folder, "txts"), pattern = ".txt")

# [1] "clovis-p ginas-1-13.txt" "cvm-p ginas-23-62.txt"
# [3] "decotelli-p ginas-9-83.txt" "rosa-p ginas-1-13.txt"

# Importar arquivos (se arquivos não tiverem marcador "End of Line" será gerado um warning
corpus <- textreuse::TextReuseCorpus( # cria uma estrutura Corpus (coleção de documentos)
dir = paste0(temp_folder, "txts"), # caminho para pasta com arquivos txt
tokenizer = tokenize_ngrams, # função para separar textos em tokens
progress = FALSE # não mostrar barra de progresso
)

Se você tiver dificuldades em importar os dados, é possível explorar os exemplos das Vignettes do pacote também.

Agora vamos inspecionar o que temos no objeto corpus:


# Exibir resultado
corpus

# TextReuseCorpus
# Number of documents: 4
# hash_func : hash_string
# tokenizer : tokenize_ngrams

Verificando a classe do objeto:


# Classe
class(corpus)

# [1] "TextReuseCorpus" "Corpus"

Quais documentos/textos temos no objeto corpus?


# Documentos importados
names(corpus)

# [1] "clovis-p ginas-1-13" "cvm-p ginas-23-62" "decotelli-p ginas-9-83"
# [4] "rosa-p ginas-1-13"

E, similarmente, podemos acessar os elementos do objeto:


# Acessando texto da dissertação de mestrado
corpus[[3]]

# TextReuseTextDocument
# file : C:\Users\ferna\AppData\Local\Temp\Rtmp6rA1wGtxts/decotelli-p ginas-9-83.txt
# hash_func : hash_string
# id : decotelli-p ginas-9-83
# minhash_func :
# tokenizer : tokenize_ngrams
# content : 1 – INTRODUÇÃO
#
# A moeda é sem dúvida a principal invenção do homem na área das ciências sociais. Criada para
# facilitar as trocas, viabilizando a especialização do trabalho, a moeda é de s

Perceba que há problemas de encoding, que para este exercício iremos simplesmente ignorar.

Índice de Jaccard

Com os dados preparados, podemos obter a estatística do Índice de Jaccard para medir a similaridade entre os textos, usando a função jaccard_similarity(), que compara um texto A com um texto B. Queremos fazer isso já para os 4, portanto usamos a função auxiliar pairwise_compare() que reporta a estatística de cada documento comparando a todos os demais (todos os pares possíveis), dentro do objeto corpus:


# Coeficientes de similaridade de Jaccard
comparisons <- textreuse::pairwise_compare(
corpus = corpus, # objeto com textos importados
f = jaccard_similarity # função para comparar os textos
)

# Resultado
comparisons

# clovis-p ginas-1-13 cvm-p ginas-23-62
# clovis-p ginas-1-13 NA 0.01308411
# cvm-p ginas-23-62 NA NA
# decotelli-p ginas-9-83 NA NA
# rosa-p ginas-1-13 NA NA
# decotelli-p ginas-9-83 rosa-p ginas-1-13
# clovis-p ginas-1-13 0.04087225 0.02439457
# cvm-p ginas-23-62 0.19385437 0.01169428
# decotelli-p ginas-9-83 NA 0.03302505
# rosa-p ginas-1-13 NA NA

O resultado de destaque mostra que o texto da dissertação de mestrado de Carlos Decotelli (decotelli-p ginas-9-83) comparado com o texto do relatório da CVM (cvm-p ginas-23-62) obteve um coeficiente de 0.19 (o valor máximo vai até 1), bem superior aos demais. Esse resultado dialoga com o que foi indicado por Thomas Conti em junho de 2020.

Vale frisar que existem outros métodos de comparação e funcionalidades que o pacote oferece, consulte a documentação.

Podemos facilmente converter o resultado, que é uma matriz, para um data frame com a função pairwise_candidates():


# Converter para data frame
comparisons_df <- textreuse::pairwise_candidates(comparisons)
comparisons_df

# A tibble: 6 x 3
# a b score
# * <chr> <chr> <dbl>
# 1 clovis-p ginas-1-13 cvm-p ginas-23-62 0.0131
# 2 clovis-p ginas-1-13 decotelli-p ginas-9-83 0.0409
# 3 clovis-p ginas-1-13 rosa-p ginas-1-13 0.0244
# 4 cvm-p ginas-23-62 decotelli-p ginas-9-83 0.194
# 5 cvm-p ginas-23-62 rosa-p ginas-1-13 0.0117
# 6 decotelli-p ginas-9-83 rosa-p ginas-1-13 0.0330

Por fim, vamos criar uma visualização dos resultados:


comparisons_df %>%
dplyr::filter(
stringr::str_detect(string = a, "decotelli") |
stringr::str_detect(string = b, "decotelli")
) %>%
dplyr::mutate(a = c("Machado-da-Silva et al.", "Relatório CVM", "Rosa & Coser")) %>%
ggplot2::ggplot(ggplot2::aes(x = reorder(a, score), y = score, fill = a)) +
ggplot2::geom_col() +
ggplot2::geom_text(
ggplot2::aes(
label = format(round(score, 2), decimal.mark = ",", )
),
position = ggplot2::position_stack(vjust = .5),
color = "white",
fontface = "bold",
size = 5
) +
ggplot2::scale_y_continuous(labels = scales::number_format(decimal.mark = ",", accuracy = 0.01)) +
ggplot2::scale_fill_manual(NULL, values = c("#b22200", "#282f6b", "#224f20")) +
ggplot2::theme_light() +
ggplot2::labs(
title = "Caso Decotelli: houve plágio?",
subtitle = "Coeficiente de similaridade com dissertação de mestrado de Decotelli (2008)",
y = "Índice de Jaccard",
x = NULL,
caption = "<span style = 'font-size:10pt'>**Dados** dos textos originais disponíveis em *Decotelli (2008)*: bit.ly/3pk8oOe*, Rosa & Coser (2003)*: bit.ly/3pqGQa4, *Machado-da-Silva et al. (1999)*: bit.ly/2Xxsjh2 e *Relatório CVM (2007)*: bit.ly/3aW5Kpz<br>**Elaboração**: analisemacro.com.br</span>"
) +
ggplot2::theme(
legend.position = "none",
axis.text = ggtext::element_markdown(size = 12, face = "bold"),
axis.title = ggtext::element_markdown(size = 12, face = "bold"),
plot.title = ggtext::element_markdown(size = 30, face = "bold", colour = "#282f6b"),
plot.subtitle = ggtext::element_markdown(size = 16),
plot.caption = ggtext::element_textbox_simple(
size = 12,
lineheight = 1,
margin = ggplot2::margin(10, 5.5, 10, 5.5)
)
)

 

 

O que achou? O pacote textreuse é bastante intuitivo e de fácil uso, de modo que até quem não tem conhecimento aprofundado de estatística, NLP, text mining, etc. consegue fazer análises simples como essa. Espero que o exercício tenha ajudado!

Referências

Recomenda-se consultar a documentação dos pacotes utilizados para entendimento aprofundado sobre as funcionalidades.

  • Jure Leskovec, Anand Rajaraman, and Jeff Ullman, Mining of Massive Datasets (Cambridge University Press, 2011).

 

Receba diretamente em seu e-mail gratuitamente nossas promoções especiais
e conteúdos exclusivos sobre Análise de Dados!

Assinar Gratuitamente