Neste post, vamos fazer uma análise de tópicos utilizando os pacotes tidytext e topicmodels. Essa análise consiste em extrair um conjunto de temas (tópicos) de um conjunto de textos. Para isso, utilizamos uma base de textos de acordos comerciais. Veremos que o método utilizado é capaz de identificar uma séria de tópicos relevantes.
É bastante comum que uma análise de dados envolva textos. Levando isso em conta, neste post, vamos trabalhar textos no formato tidy utilizando o pacote tidytext
. Após a manipulação de dados, vamos estimar um modelo de tópicos utilizando o pacote topicmodels
. Esta abordagem permite encontrar diversos “temas” dentro de um conjunto de textos. Este post segue o que está descrito no livro Tidy Text Mining with R que foi escrito pelos mesmos autores do pacote tidytext
.
Como aplicação, utilizaremos textos de acordos comerciais da base Text of Trade Agreements - TOTA desenvolvida pela UNCTAD, que abrange 451 acordos comerciais. Os acordos comerciais celebrados ao redor do mundo notificados à Organização Mundial de Comércio (OMC) possuem um texto que descreve os diversos aspectos que foram negociados e acertados entre os diferentes parceiros. Os acordos comerciais cobrem desde questões tarifárias a questões de propriedade intelectual.
Vamos começar a nossa análise!
Aqui estão listados os pacotes que utilizamos neste post.
library(tidyverse)
library(tidytext)
library(xml2)
library(topicmodels)
library(lubridate)
library(hrbrthemes)
A função theme_set()
define o tema para todos os gráficos do ggplot2.
theme_set(
theme_ipsum() +
theme(
legend.position = "top",
legend.justification = "left"
)
)
Os dados estão disponíveis neste link: https://github.com/mappingtreaties/tota.
São 451 arquivos xml com os textos de cada acordo. Por exemplo:
Assim, precisaremos ler cada um deles e convertê-los para data.frame. Dessa forma, vamos primeiro criar um vetor com os caminhos dos arquivos:
arquivos_pta <- list.files('xml/', full.names = TRUE)
arquivos_pta[1:3]
[1] "xml//pta_1.xml" "xml//pta_10.xml" "xml//pta_100.xml"
A função ler_texto_acordo()
recebe como input o caminho de um arquivo e retorna um data.frame com os dados de um acordo. Cada linha do data.frame
representa um capítulo do acordo. Na tabela abaixo, o leitor pode dar uma explorada nas variáveis que criamos.
ler_texto_acordo <- function(arquivo){
pta <- read_xml(arquivo)
pta_id <- pta %>%
xml_find_all("//wto_rta_id") %>%
xml_text()
pta_nome <- pta %>%
xml_find_all("//name") %>%
xml_text()
pta_status <- pta %>%
xml_find_all("//status") %>%
xml_text()
pta_data_assinatura <- pta %>%
xml_find_all("//date_signed") %>%
xml_text()
pta_lingua <- pta %>%
xml_find_all("//language") %>%
xml_text()
capitulos <- pta %>%
xml_find_all("//chapter")
texto_capitulos <- capitulos %>%
map(xml_contents) %>%
map_chr(~{
.x %>%
map_chr(xml_text) %>%
paste0(collapse = " \n ")
})
capitulos %>%
map(xml_attrs) %>%
map_df(as.list) %>%
mutate(
pta_id = pta_id,
pta_nome = pta_nome,
pta_status = pta_status,
pta_data_assinatura = ymd(pta_data_assinatura),
id_capitulo = paste0(pta_id, "_", chapter_identifier),
ano = year(pta_data_assinatura),
pta_lingua = pta_lingua,
texto = texto_capitulos
) %>%
select(contains("pta"), everything())
}
ler_texto_acordo(arquivos_pta[5]) %>%
rmarkdown::paged_table()
Agora, vamos usar a função map_df()
para ler todos os arquivos e combiná-los em um único data.frame
.
pta_df <- map_df(arquivos_pta, ler_texto_acordo)
Os acordos podem estar em várias línguas:
pta_df %>%
select(pta_id, pta_lingua) %>%
distinct() %>%
count(pta_lingua)
# A tibble: 3 x 2
pta_lingua n
<chr> <int>
1 en 424
2 es 24
3 fr 2
Para a nossa análise, vamos nos restringir aos textos que estão em inglês:
pta_df <- pta_df %>%
filter(pta_lingua == "en")
Com a nossa base preparada, vamos checar quantos acordos foram assinados por ano:
pta_df %>%
select(pta_id, ano) %>%
distinct() %>%
ggplot(aes(x = ano)) +
geom_bar(fill = "#2980b9") +
scale_x_continuous(breaks = seq(1950, 2010, 10)) +
labs(
title = "Número de Acordos Comerciais Assinados por Ano",
subtitle = "Os acordos comerciais se proliferaram a partir da década de 90.
A Organização Mundial de Comércio (OMC) foi criada em 1995.",
x = "Ano",
y = "Número de Acordos"
)
Para os autores do pacote tidytext
, o formato de dados tidy
para textos seria aquele de uma tabela com uma linha por token. Aqui, token seria uma unidade de texto relevante (caractere, palavra, parágrafo etc.). Neste post, a nossa unidade será a de palavras. Assim, teremos um data.frame com uma linha para cada palavra de cada documento. Aqui, definimos “documento” como um capítulo de um acordo específico (coluna id_capitulo
).
Adicionalmente, removemos as chamadas stop words com um anti_join()
. Veja abaixo o que seria esse objeto stop_words
:
head(stop_words, 10)
# A tibble: 10 x 2
word lexicon
<chr> <chr>
1 a SMART
2 a's SMART
3 able SMART
4 about SMART
5 above SMART
6 according SMART
7 accordingly SMART
8 across SMART
9 actually SMART
10 after SMART
Além das stop words, as palavrar que possuem números também são removidas.
textos_df <- pta_df %>%
unnest_tokens(word, texto) %>%
select(id_capitulo, word) %>%
anti_join(stop_words) %>%
filter(!str_detect(word, "[0-9]"))
dim(textos_df)
[1] 2567029 2
head(textos_df)
# A tibble: 6 x 2
id_capitulo word
<chr> <chr>
1 1_1 preamble
2 1_1 japan
3 1_1 kingdom
4 1_1 thailand
5 1_1 hereinafter
6 1_1 referred
A função unnest_tokens()
irá quebrar a variável texto
por palavras. Ao todo, temos 2567029 palavras. Veja que cada linha agora é uma palavra.
Abaixo é criado um data.frame com as 2000 palavras mais frequentes. As palavras party e parties são bastante frequentes, mas não trazem nenhuma informação relevante. Dessa forma, elas são removidas da lista de palavras mais frequentes..
palavras_frequentes <- textos_df %>%
filter(!word %in% c("party", "parties")) %>%
count(word) %>%
top_n(2000) %>%
pull(word)
Com os nossos dados quase prontos, vamos falar sobre o método que será utilizado: Latent Dirichlet Allocation (LDA). O nosso objetivo é encontrar os tópicos que existiriam em uma coleção de documentos. Neste modelo, define-se um quantidade de tópicos \(k\) e o algoritmo irá estimar a distribuição de termos (\(\beta\)) de cada tópico. Esta distribuição traz a probabilidade de ocorrência de uma palavra em um determinado tópico.
Além disso, cada documento é uma mistura de tópicos. Por exemplo, supondo 2 tópicos, o documento 1 é 60% do tópico X e 40% do tópico Y. O que o LDA faz é estimar essas distribuições a partir da frequência de palavras dentro de cada documento.
Para maiores detalhes, ver o capítulo 6 e o vignette do topicmodels
(vignette("topicmodels")
).
Para estimar o modelo, é preciso uma matriz de termos por documento. Cada linha dessa matriz é um documento e cada coluna é um termo (uma palavra). O valor em cada posição é a frequência de um termo em um documento. A função cast_dtm()
converte o data.frame para uma matriz de termos por documento.
textos_dtm <- textos_df %>%
filter(word %in% palavras_frequentes) %>%
group_by(id_capitulo, word) %>%
summarise(n = n()) %>%
cast_dtm(document = id_capitulo, term = word, value = n)
A função abaixo estima o modelo. Além dos dados, definimos arbitrariamente o número de tópicos em 16 e fixamos a seed para possibilitar a reprodutibilidade dos resultados.
acordos_lda <- LDA(textos_dtm, k = 16, control = list(seed = 9876))
Com o modelo estimado, pode-se extrair o \(\beta\), que é a probabilidade estimada de cada termo dentro de um determinado tópico.
termos_acordos <- tidy(acordos_lda, type = "beta")
Dessa forma, podemos extrair os termos com as maiores probabilidades dentro de cada tópico. Assim, é possível checar se essas palavras indicam sobre qual assunto cada tópico está relacionado. Para quem trabalha no dia a dia com acordos comerciais, deve ser fácil indicar os tópicos. Mesmo não sendo especialista, é possível facilmente identificar que, por exemplo, o tópico 1 refere-se a compras públicas. Já o tópico 7 indica que seja algo relacionado ao tema de propriedade intelectual.
#https://stackoverflow.com/questions/28436855/change-the-number-of-breaks-using-facet-grid-in-ggplot2
equal_breaks <- function(n = 3, s = 0.05, ...){
function(x){
# rescaling
d <- s * diff(range(x)) / (1+2*s)
seq(min(x)+d, max(x)-d, length=n)
}
}
termos_acordos %>%
group_by(topic) %>%
top_n(10, beta) %>%
ungroup() %>%
arrange(topic, -beta) %>%
mutate(
term = reorder_within(term, beta, topic),
topic = paste0("Tópico: ", topic),
topic = factor(topic, levels = paste0("Tópico: ", 1:16))
) %>%
ggplot(aes(y = term, x = beta, color = topic)) +
geom_point(size = 2, show.legend = FALSE) +
geom_segment(
mapping = aes(yend = term),
xend = 0,
linetype = "dotted",
show.legend = FALSE
) +
facet_wrap(~ topic, scales = "free") +
scale_y_reordered() +
scale_x_continuous(
breaks = equal_breaks(n = 3, s = 0.01),
labels = scales::number_format(big.mark = ".", decimal.mark = ",", accuracy = 0.01)
) +
labs(
title = "Principais termos de cada tópico extraído dos textos dos\nacordos comerciais",
subtitle = "O beta é a probabilidade estimada de um determinado termo ter sido gerado em cada tópico específico.",
y = "Termo"
) +
theme(
panel.grid.major.y = element_blank(),
panel.grid.minor.x = element_blank()
) +
theme_ipsum(
axis_text_size = 8
)
Também é possível extrair a proporção de cada tópico em cada documento. No nosso caso, um documento refere-se a um capítulo de um acordo. Essa base tem a grande vantagem que a maioria dos capítulos já estão rotulados. A única dificuldade é que existem mais de 900 rótulos diferentes. Muitas vezes, trata-se do mesmo tema, mas com uma escrita diferente. De toda forma, podemos verificar para quais rótulos os nossos tópicos estão associados.
Primeiro vamos extrair a proporção de cada tópico em cada documento:
capitulo_topico <- tidy(acordos_lda, matrix = "gamma")
Agora, vamos combinar o objeto capitulo_topico
com o data.frame de acordos (pta_df
), selecionando as colunas pta_nome
, id_capitulo
, ano
e name
.
capitulo_topico <- capitulo_topico %>%
left_join(
pta_df %>%
select(pta_nome, id_capitulo, ano, name),
by = c("document" = "id_capitulo")
)
Por fim, iremos remover os capítulos que não estão rotulados e vamos extrair o documento que tem a maior proporção de cada tópico. Depois mostramos cada tópico e o rótulo que consta na base de dados dos textos de acordos. O resultado está bem alinhado com o esperaríamos olhando o gráfico anterior. Por exemplo, o tópico 10 claramente refere-se ao tópico de “Defesa Comercial”. Na base, o capítulo que tem a maior proporção deste tópico está rotulado como “Safeguard Measures”.
capitulo_topico %>%
filter(!is.na(name)) %>%
group_by(topic) %>%
filter(gamma == max(gamma)) %>%
select(topic, name)
# A tibble: 16 x 2
# Groups: topic [16]
topic name
<int> <chr>
1 1 Government Procurement
2 2 Financial Services
3 3 National Treatment And Market Access For Goods
4 4 Rules Of Origin
5 5 Customs Procedures
6 6 Investment
7 7 Intellectual Property Rights
8 8 Institutional And Final Provisions
9 9 Technical Barriers To Trade
10 10 Safeguard Measures
11 11 General Provisions
12 12 Industrial Products
13 13 Preamble
14 14 The Common Market For Eastern And Southern Africa
15 15 Telecommunications Services
16 16 Dispute Settlement
Para finalizar a nossa análise, vamos verificar a dinâmica da frequência de acordos que incluíram ou não um capítulo sobre Propriedade Intelectual. Para isso, vamos considerar que um acordo possui um capítulo sobre Propriedade Intelectual se existe algum capítulo com uma proporção desse tópico superior a 50%. Verificamos que esse tema ficou mais relevante após 2005, período que se verifica uma maior frequência de acordos que abrangem esse tema.
capitulo_topico %>%
mutate(ip = topic == 7 & gamma > .5) %>%
group_by(pta_nome, ano) %>%
summarise(ip = max(ip)) %>%
ungroup() %>%
count(ano, ip) %>%
mutate(ip = ifelse(ip == 1, "Sim", "Não"),
ip = factor(ip, levels = c("Sim", "Não"))) %>%
ggplot(aes(x = ano, y = n, fill = ip)) +
geom_col() +
scale_fill_manual(values = c("#e55039", "#b2bec3")) +
scale_x_continuous(breaks = seq(1950, 2020, 10)) +
labs(
title = "Evolução de acordos comerciais identificados com ou
sem tópicos sobre propriedade intelectual",
x = "Ano",
y = "Frequência",
fill = "Acordo com Tópico\nde Propriedade Intelectual?"
)
Neste post, trabalhamos com dados textuais no R e mostramos como é possível estimar um modelo de tópicos. Usamos uma base de dados de textos de acordos comerciais, mas a aplicação pode ser facilmente transferida para outros contextos.
Existem diversas outras aplicações relacionadas a Processamento de Linguagem Natural que poderão ser apresentadas em outros posts. Hoje, ficamos por aqui!
For attribution, please cite this work as
Alencar (2019, Oct. 17). Fulljoin: Tidytext e Modelagem de Tópicos com o R: Analisando os Textos de Acordos Comerciais. Retrieved from https://www.fulljoin.com.br/posts/2019-09-19-modelagem-de-tpicos-com-o-r-analisando-os-textos-de-acordos-comerciais/
BibTeX citation
@misc{alencar2019tidytext, author = {Alencar, Paulo Felipe}, title = {Fulljoin: Tidytext e Modelagem de Tópicos com o R: Analisando os Textos de Acordos Comerciais}, url = {https://www.fulljoin.com.br/posts/2019-09-19-modelagem-de-tpicos-com-o-r-analisando-os-textos-de-acordos-comerciais/}, year = {2019} }