Nesse post nós iremos fazer uma simples (e rápida) análise do dataset “ISIS Religious Texts v1”, que pode ser encontrado https://www.kaggle.com/fifthtribe/isis-religious-texts. Essa base de dados contém textos de duas revistas do ISIS, estas sendo a “Dabiq” e a “Rumyiah”, que são (ou eram?) usadas para propaganda política e recrutamento de novos membros para o grupo terrorista.

Primeiro começamos carregando as bibliotecas essenciais para a tarefa, com atenção à Rlang, que nos será bem util. Você pode conferir melhor suas funcionalidades nessa publicação https://www.tidyverse.org/blog/2018/03/rlang-0.2.0/, feita pelo time de desenvolvedores do Tidyverse.

library(tidyverse) # É uma meta-biblioteca, isto é, uma bib. que contém outras bibliotecas.
library(tm) # Text mining no geral, faz de tudo um pouco.
library(topicmodels) # Para criar o modelo LDA.
library(tidytext) # Ferramentas de tidy NLP.
library(SnowballC) # Para o stemming!
library(magrittr) # NEW TOOLS!
library(rlang)
library(forcats)

Então vamos carregar os dados na memória, usando o {readr}, que já vem incluso na meta-biblioteca {tidyverse}. Contudo, para evitar problemas de compatibilidade de caracteres, visto que podemos ter texto em árabe, já me antecipo e altero o https://en.wikipedia.org/wiki/Character_encoding da coluna que contém o texto para UTF-8, que é o ideal para as bibliotecas de análise de texto que serão usadas (especialmente a {tm}). Em diversas situações, podem ocorrer erros durante a análise devido ao texto estar no encoding errado, a alteração deste – embora tenha um motivo claro, e preventivo, neste dataset – pode ser útil em outros contextos também.

dataset = readr::read_csv('../../data/ISIS Religious Texts v1.csv') # Carrega os dados

dataset$Quote = iconv(dataset$Quote, "ASCII", "UTF-8", sub="byte") # Altera o encoding

head(dataset)
## # A tibble: 6 x 8
##   Magazine Issue Date   Type  Source   Quote           Purpose `Article Name`   
##   <chr>    <dbl> <chr>  <chr> <chr>    <chr>           <chr>   <chr>            
## 1 Dabiq        1 Jun-14 Jiha… Abu Mus… The spark has … Support First Page       
## 2 Dabiq        1 Jun-14 Hadi… Sahih M… <93>The Hour w… Support Introduction     
## 3 Dabiq        1 Jun-14 Jiha… Abu Mus… <93>The spark … Support Introduction     
## 4 Dabiq        1 Jun-14 Jiha… Abu Bak… <93>O Muslims … Support Khilafah Declared
## 5 Dabiq        1 Jun-14 Jiha… Abu Bak… <93>O Ummah of… Support The World has Di…
## 6 Dabiq        1 Jun-14 Jiha… Abu Bak… <93>Therefore,… Support A Call to Hijrah

Ao observar o dataset vemos que ele possui 8 colunas e aproximadamente 2.700 linhas. Informações como data (2014-2016), revista, tipo e propósito são insumos valiosos, e não serão usadas aqui para mantermos esta publicação em um tamanho restrito. Uma ideia que não será abordada nessa publicação é observarmos se há alteração do discurso conforme a situação política do ISIS se agrava, será que o tom passou a ser mais messiânico? Não sei, fica o exercicio ao leitor.

Enfim, passemos ao código…

Como sempre começamos com uma função, dessa vez crio uma chamada cleaner_impurities, o que ela faz é simples. Ela passa o texto pela função stringr::str_replace_all(), que substitui um elemento expresso por meio de expressão regular, por outro. No código abaixo, entra texto, que é passado pela função anteriormente descrita, e então retornado sem pontuação, com um espaço no lugar.

cleaner_impurities = function(text){
  text %>% 
    stringr::str_replace_all("[[:punct:]]", " ") 
  text
}

Mas calma… Você, pessoa bonita e atenta, poderia me chamar atenção ao fato de que existe a função tm::map(removePunctuation), que ao passarmos no https://en.wikipedia.org/wiki/Text_corpus atinge o mesmo objetivo. Concordo, mas estava afim de brincar com o rlang, e também, a função também dá uma utilidade às https://alphabetsigma.netlify.app/2020/03/17/introducao-as-expressoes-regulares/.

treat = function(data, texto_col= Quote){
  
 data %>%
    mutate({{ texto_col }} := cleaner_impurities({{ texto_col }})) %>% 
    pull({{ texto_col }}) -> data_res
  
  data_res
}

A sintaxe da função acima pode ser um pouco nova para alguns, os operadores {{ }} (lê-se Curly-Curly) e := nos permitem utilizarmos os nomes das colunas dos dataframes como argumentos, sem precisarmos recorrer a construções de código obscuras, como é possível ver nesta https://www.brodrigues.co/blog/2019-06-20-tidy_eval_saga/.

Mas, o que essa função faz é bem simples. A função aceita dois argumentos, data e texto_col, a primeira corresponde ao data.frame/tibble que contém os dados a serem tratados, a última corresponde ao nome da coluna qual está o texto desejado. Altera a coluna ‘Quote’, passando-a pela função cleaner_impurities, e então retorna ela como um vetor.

Preste atenção ao fato de que a informação do nome da coluna não deve ser inserida como uma 'string', e sim como se fosse um objeto, https://stackoverflow.com/questions/61377657/tidy-evaluation-not-working-with-mutate-and-stringr.

topTerms = function(text, n_topics=3){
  
   text %>%
    SnowballC::wordStem(language= 'english') %>% 
    tm::VectorSource() %>% # Interpreta cada vetor como um documento.
    tm::Corpus() %>%  # Cria um corpus com os documentos
    tm::tm_map(tolower) %>%  # Transforma todos os documentos em letras minusculas
    tm::tm_map(removePunctuation) %>% 
    tm::tm_map(removeNumbers) %>% # Remove os números
    tm::tm_map(removeWords, stopwords('english')) %>% # Remove palavras frequentes e que são vazias de significado (the, it, as, etc.)
    tm::DocumentTermMatrix() -> DTM # Transforma em uma Document Term Matrix.
  
  indices_unicos <- unique(DTM$i) # index de cada valor único.
  DTM <- DTM[indices_unicos, ] # pega um subset desses indexs
  
  topterms <- topicmodels::LDA(DTM, k = n_topics) %>%  #Roda o LDA com 3 tópicos
    tidytext::tidy(matrix= 'beta') %>% # Transforma o resultado do modelo em um objeto 'tidy', manipulável.
    group_by(topic) %>% 
    top_n(20, beta) %>% # Seleciona os 15 maiores betas.
    ungroup %>% 
    arrange(topic, -beta)
  
  topterms # Retorna.

}

res = dataset %>% 
  treat() %>% 
  topTerms()

    
res
## # A tibble: 60 x 3
##    topic term         beta
##    <int> <chr>       <dbl>
##  1     1 allah     0.0468 
##  2     1 will      0.0359 
##  3     1 said      0.0184 
##  4     1 people    0.0109 
##  5     1 indeed    0.0108 
##  6     1 ibn       0.00961
##  7     1 messenger 0.00770
##  8     1 one       0.00765
##  9     1 say       0.00692
## 10     1 upon      0.00656
## # … with 50 more rows

O trecho acima é bem auto-explicativo, até mesmo devido aos comentários no código. Informações extras podem ser encontradas na documentação das funções.

Vamos plotar as informações que conseguimos, assim é possível visualizarmos melhor o resultado.

 plt = res %>% # take the top terms
          mutate(term = reorder(term, beta)) %>%
          ggplot(aes(term, beta, fill = factor(topic))) + 
          geom_col(show.legend = FALSE) + 
          facet_wrap(~ topic, scales = "free") + 
          labs(x = NULL, y = "Beta") +  
          coord_flip()
  
plt

Como esperado, temos quatro grupos, porém, ainda temos muita poluição nos dados. Irei adicionar alguns passos adicionais, e tentaremos ter um resultado mais limpo.

cleaned_docs = function(data){
  data %>% 
    pull({{ Quote }}) %>% 
    cleaner_impurities %>% 
    VectorSource() %>% 
    Corpus() %>% 
    DocumentTermMatrix() %>% 
    tidy() %>% 
    anti_join(stop_words, by = c('term'= 'word')) %>% 
    group_by(document) %>% 
    mutate(terms = toString(rep(term, count))) %>% 
    select(document,terms) %>% 
    unique()
}

final = topTerms(res) %>% 
  mutate(term = reorder(term, beta)) %>%
  ggplot(aes(term, beta, fill = factor(topic))) + 
  geom_col(show.legend = FALSE) + 
  facet_wrap(~ topic, scales = "free") + 
  labs(x = NULL, y = "Beta") +  
  coord_flip()
## Warning in tm_map.SimpleCorpus(., tolower): transformation drops documents
## Warning in tm_map.SimpleCorpus(., removePunctuation): transformation drops
## documents
## Warning in tm_map.SimpleCorpus(., removeNumbers): transformation drops documents
## Warning in tm_map.SimpleCorpus(., removeWords, stopwords("english")):
## transformation drops documents
final

Ok, temos algo melhor. Observando os gráficos na ordem, podemos ter ideia do que cada um dos 4 tópicos abordam, porém não temos nenhuma surpresa advinda deste dataset. Todos nós temos nossos conhecimentos de grupos terroristas, sabemos muito bem como o discurso funciona, devido ao nosso extenso aprendizado com Call of Duty e filmes americanos.

No terceiro, as mensagens tem relação mais intima com assuntos estritamente religiosos. Palavras como Allah, messenger, prophet são de cunho religioso, o que indica que provavelmente esses textos estariam classificados como “Quran”, no dataset. Também mostra que é possível traçar uma distinção entre textos que tratam exclusivamente de religião, é possível acreditar que, por serem textos utilizados por grupos terroristas, estejam relacionados a uma suposta glória da causa.

Em segundo lugar, temos, também, uma infinidade de termos religiosos. Atenção nas palavras people, security, syrian, isis, iraq, jihd (jihad). Por esses termos, podemos imaginar que aqui o tema deixa de ser estritamente a religião, e passa a ser as motivações religiosas que são usadas como desculpa para o fazer a guerra, com especial foco à Síria.

Na primeira coluna, temos palavras importantes, dando destaque à attawbah, esta é a nona Surata do Alcorão, e trata de reparação e trata do balanço e dos problemas da harmonia e da guerra. O grupo de documentos parece ter relação com o sacrificio necessário pela guerra.

Conclusão

No fim, concluo que talvez usar o LDA nesse grupo de documentos não tenha sido a melhor escolha. Voltarei a este dataset no futuro, porém, talvez usando TF-IDF para fazer a análise, espero ter resultados mais satisfatórios. O fato de todos os textos possuirem uma densa carga de discurso religioso pode ter atrapalhado os resultados do modelo, o tamanho dos textos também é um problema, o LDA performa mal em textos curtos. Fica o aprendizado.