Markov-Chain Carluxo - Criando um bot para Twitter usando o algorítmo de Markov-Chain

Fri, May 1, 2020 13-minute read
no

O primeiro passo a ser feito é instalar e carregar três bibliotecas, a tidyverse, rtweet, markovchain, caso você não tenha-as instaladas instale-as direto do CRAN usando a função install.packages(). Não sabe como fazer? Digite help(install.packages) no console! Cada uma tem uma funcionalidade:

  • Tidyverse: Uma metabiblioteca que contém um conjunto de ferramentas que adicionam uma nova sintaxe no R. Por meio delas é possível escrever um código mais limpo e eficiente.

  • rtweet: É a interface entre R e a API do twitter, é ela que será utilizada para capturarmos os tweets e depois fazermos a publicação de um novo.

  • markovchain: é a biblioteca que implementa em R o algoritmo que será utilizado para a geração do texto. Eu poderia fazer explicações acerca da lógica que está por trás, porém, existem excelentes explicações onlines como 1. esta explicação visual (caso você goste de imagens); e 2. essa mais teórica, feita por acadêmicos de Princeton.

  • tictoc: será utilizada para marcarmos tempo, não é necessária para o funcionamento do modelo.

library(tidyverse)
library(rtweet)
library(markovchain)
library(tictoc)

Carregadas as bibliotecas, vamos fazer o login na API do twitter. Se você não possui as credenciais necessárias, sem problemas é bem fácil criar, e - descontado o tempo que o Twitter levar demorar para liberar sua conta de desenvolvedor - não deve demorar mais de 10 minutos. Tem um videozinho aqui explicando, é em inglês, mas pode ser assistido sem som, nada essencial de ser ouvido.

rtweet::create_token(consumer_key = 'CKQPkCiBAoiluqzi33PMTYV6p',
                     consumer_secret = 'GyCA2voQW1lSxgA8EG1FCdpeCKkxAM6eZfROvlg5HoPeKw30vJ',
                     access_token = '1191556232701714433-qQpNy6b7TZTPrjGpkqJmg8J3dmYLHY',
                     access_secret = 'PxWPn7YvudptAJuZXEfnO840Ov5nSgdRrcE1feOQvCWdC',
                     app = 'carluxobot') # essas keys foram invalidadas :)
## <Token>
## <oauth_endpoint>
##  request:   https://api.twitter.com/oauth/request_token
##  authorize: https://api.twitter.com/oauth/authenticate
##  access:    https://api.twitter.com/oauth/access_token
## <oauth_app> carluxobot
##   key:    CKQPkCiBAoiluqzi33PMTYV6p
##   secret: <hidden>
## <credentials> oauth_token, oauth_token_secret
## ---
tweets_carluxo <- rtweet::get_timeline('carlosbolsonaro', n = 3200)

No código acima temos duas funções, a primeira é a que faz o Login na API do twitter, utilizando os dados que pegamos lá no app que criamos. A partir disso, nós usamos a função get_timeline, do {rtweet}, para coletar os últimos 3200 tweets feitos pelo Vereador Federal Carlos Bolsonaro. Este número, 3200, não foi escolhido por acaso, ele é o número máximo de tweets passíveis de serem coletados por 1 requisição, isso na versão gratuita da API do Twitter.

Isso vai nos retornar um dataframe bem grande, qual só iremos utilizar uma coluna, mas o leitor é livre para explorá-lo e encontrar outras utilidades. Veja-o.

user_idstatus_idcreated_atscreen_nametextsourcedisplay_text_widthreply_to_status_idreply_to_user_idreply_to_screen_nameis_quoteis_retweetfavorite_countretweet_countquote_countreply_counthashtagssymbolsurls_urlurls_t.courls_expanded_urlmedia_urlmedia_t.comedia_expanded_urlmedia_typeext_media_urlext_media_t.coext_media_expanded_urlext_media_typementions_user_idmentions_screen_namelangquoted_status_idquoted_textquoted_created_atquoted_sourcequoted_favorite_countquoted_retweet_countquoted_user_idquoted_screen_namequoted_namequoted_followers_countquoted_friends_countquoted_statuses_countquoted_locationquoted_descriptionquoted_verifiedretweet_status_idretweet_textretweet_created_atretweet_sourceretweet_favorite_countretweet_retweet_countretweet_user_idretweet_screen_nameretweet_nameretweet_followers_countretweet_friends_countretweet_statuses_countretweet_locationretweet_descriptionretweet_verifiedplace_urlplace_nameplace_full_nameplace_typecountrycountry_codegeo_coordscoords_coordsbbox_coordsstatus_urlnamelocationdescriptionurlprotectedfollowers_countfriends_countlisted_countstatuses_countfavourites_countaccount_created_atverifiedprofile_urlprofile_expanded_urlaccount_langprofile_banner_urlprofile_background_urlprofile_image_url
6871257612563357220526489601588368752CarlosBolsonaroTwitter for Android18NANANAFALSEFALSE51121018NANANANANANANAhttp://pbs.twimg.com/media/EW9mk1XWoAIm3nW.jpghttps://t.co/tgRoWDQwIchttps://twitter.com/CarlosBolsonaro/status/1256335722052648960/photo/1photohttp://pbs.twimg.com/media/EW9mk1XWoAIm3nW.jpghttps://t.co/tgRoWDQwIchttps://twitter.com/CarlosBolsonaro/status/1256335722052648960/photo/1NAc(“861707648584085504”, “37717107”)c(“govbr”, “minsaude”)undNANANANANANANANANANANANANANANANANANANANANANANANANANANANANANANANANANANANAc(NA, NA)c(NA, NA)c(NA, NA, NA, NA, NA, NA, NA, NA)https://twitter.com/CarlosBolsonaro/status/1256335722052648960Carlos BolsonaroRio de Janeiro-RJVereador da cidade do Rio de Janeiro (ainda podendo opinar sobre o que achar pertinente).https://t.co/3z2ZiPnA8fFALSE179052951020461535141271251212007TRUEhttps://t.co/3z2ZiPnA8fhttp://www.carlosbolsonaro.com.brNAhttps://pbs.twimg.com/profile_banners/68712576/1575806653http://abs.twimg.com/images/themes/theme1/bg.pnghttp://pbs.twimg.com/profile_images/1230681290120318977/iI2UkUQm_normal.jpg
6871257612562069314605875221588338046CarlosBolsonarohttps://t.co/GcdJFTYEgYTwitter for Android0NANANAFALSEFALSE202183309NANANANANANANAhttp://pbs.twimg.com/ext_tw_video_thumb/1256206860748828676/pu/img/0zd0Mn48lBIBpbv7.jpghttps://t.co/GcdJFTYEgYhttps://twitter.com/CarlosBolsonaro/status/1256206931460587522/video/1photohttp://pbs.twimg.com/ext_tw_video_thumb/1256206860748828676/pu/img/0zd0Mn48lBIBpbv7.jpghttps://t.co/GcdJFTYEgYhttps://twitter.com/CarlosBolsonaro/status/1256206931460587522/video/1NANANAundNANANANANANANANANANANANANANANANANANANANANANANANANANANANANANANANANANANANAc(NA, NA)c(NA, NA)c(NA, NA, NA, NA, NA, NA, NA, NA)https://twitter.com/CarlosBolsonaro/status/1256206931460587522Carlos BolsonaroRio de Janeiro-RJVereador da cidade do Rio de Janeiro (ainda podendo opinar sobre o que achar pertinente).https://t.co/3z2ZiPnA8fFALSE179052951020461535141271251212007TRUEhttps://t.co/3z2ZiPnA8fhttp://www.carlosbolsonaro.com.brNAhttps://pbs.twimg.com/profile_banners/68712576/1575806653http://abs.twimg.com/images/themes/theme1/bg.pnghttp://pbs.twimg.com/profile_images/1230681290120318977/iI2UkUQm_normal.jpg
6871257612561227668158709761588317979CarlosBolsonaro@MarcosQuezado1 Se vi essa mulher na casa do meu pai 4 vezes foi muito. O que todos viam era alguém mais preocupada em se filmar em eventos públicos ao lado de Jair Bolsonaro para se promover e depois fazer o que fez, como muitos. Minha irmã!? É muito cara de pau, Meu Deus!Twitter for iPhone25812561133819431936071148318806303027205MarcosQuezado1FALSEFALSE66341068NANANANANANANANANANANANANANANA1148318806303027205MarcosQuezado1ptNANANANANANANANANANANANANANANANANANANANANANANANANANANANANANANANANANANANAc(NA, NA)c(NA, NA)c(NA, NA, NA, NA, NA, NA, NA, NA)https://twitter.com/CarlosBolsonaro/status/1256122766815870976Carlos BolsonaroRio de Janeiro-RJVereador da cidade do Rio de Janeiro (ainda podendo opinar sobre o que achar pertinente).https://t.co/3z2ZiPnA8fFALSE179052951020461535141271251212007TRUEhttps://t.co/3z2ZiPnA8fhttp://www.carlosbolsonaro.com.brNAhttps://pbs.twimg.com/profile_banners/68712576/1575806653http://abs.twimg.com/images/themes/theme1/bg.pnghttp://pbs.twimg.com/profile_images/1230681290120318977/iI2UkUQm_normal.jpg
6871257612561196829119201291588317244CarlosBolsonaro@ClaudeLuca_ @CrisBernart @FMouraBrasil @RevistaCrusoe @RevistaISTOE @VEJA Tudo engatado um no outro… acuse os do que você é…..Twitter for iPhone561256091079343996929186240150ClaudeLuca_FALSEFALSE554109NANANANANANANANANANANANANANANAc(“186240150”, “1081209715923795968”, “52849416”, “967904717446766593”, “29913589”, “17715048”)c(“ClaudeLuca_”, “CrisBernart”, “FMouraBrasil”, “RevistaCrusoe”, “RevistaISTOE”, “VEJA”)ptNANANANANANANANANANANANANANANANANANANANANANANANANANANANANANANANANANANANAc(NA, NA)c(NA, NA)c(NA, NA, NA, NA, NA, NA, NA, NA)https://twitter.com/CarlosBolsonaro/status/1256119682911920129Carlos BolsonaroRio de Janeiro-RJVereador da cidade do Rio de Janeiro (ainda podendo opinar sobre o que achar pertinente).https://t.co/3z2ZiPnA8fFALSE179052951020461535141271251212007TRUEhttps://t.co/3z2ZiPnA8fhttp://www.carlosbolsonaro.com.brNAhttps://pbs.twimg.com/profile_banners/68712576/1575806653http://abs.twimg.com/images/themes/theme1/bg.pnghttp://pbs.twimg.com/profile_images/1230681290120318977/iI2UkUQm_normal.jpg
6871257612561073749197783041588314310CarlosBolsonaro@kimpaim Interessante!Twitter for Android13125610261330657689675264300kimpaimFALSEFALSE4525418NANANANANANANANANANANANANANANA75264300kimpaimptNANANANANANANANANANANANANANANANANANANANANANANANANANANANANANANANANANANANAc(NA, NA)c(NA, NA)c(NA, NA, NA, NA, NA, NA, NA, NA)https://twitter.com/CarlosBolsonaro/status/1256107374919778304Carlos BolsonaroRio de Janeiro-RJVereador da cidade do Rio de Janeiro (ainda podendo opinar sobre o que achar pertinente).https://t.co/3z2ZiPnA8fFALSE179052951020461535141271251212007TRUEhttps://t.co/3z2ZiPnA8fhttp://www.carlosbolsonaro.com.brNAhttps://pbs.twimg.com/profile_banners/68712576/1575806653http://abs.twimg.com/images/themes/theme1/bg.pnghttp://pbs.twimg.com/profile_images/1230681290120318977/iI2UkUQm_normal.jpg
6871257612560785572033863681588307439CarlosBolsonaroBelo Horizonte (30/04/2020). Via @taoquei1 https://t.co/BowdIhGrljTwitter for Android42NANANAFALSEFALSE183494696NANANANANANANAhttp://pbs.twimg.com/ext_tw_video_thumb/1256078371030798339/pu/img/GvaHj1cQSD9F4Scn.jpghttps://t.co/BowdIhGrljhttps://twitter.com/CarlosBolsonaro/status/1256078557203386368/video/1photohttp://pbs.twimg.com/ext_tw_video_thumb/1256078371030798339/pu/img/GvaHj1cQSD9F4Scn.jpghttps://t.co/BowdIhGrljhttps://twitter.com/CarlosBolsonaro/status/1256078557203386368/video/1NA1087259768taoquei1ptNANANANANANANANANANANANANANANANANANANANANANANANANANANANANANANANANANANANAc(NA, NA)c(NA, NA)c(NA, NA, NA, NA, NA, NA, NA, NA)https://twitter.com/CarlosBolsonaro/status/1256078557203386368Carlos BolsonaroRio de Janeiro-RJVereador da cidade do Rio de Janeiro (ainda podendo opinar sobre o que achar pertinente).https://t.co/3z2ZiPnA8fFALSE179052951020461535141271251212007TRUEhttps://t.co/3z2ZiPnA8fhttp://www.carlosbolsonaro.com.brNAhttps://pbs.twimg.com/profile_banners/68712576/1575806653http://abs.twimg.com/images/themes/theme1/bg.pnghttp://pbs.twimg.com/profile_images/1230681290120318977/iI2UkUQm_normal.jpg

Bonito, né? A API do Twitter nos retorna diversas informações que podem ser utilizadas para vários tipos de análises diferentes, vale a pena dar uma olhada no que o pessoal da DAPP/FGV tem feito com eles. Mas só a coluna text será utilizada por nós. Passemos ao código…

clean_tweets <- function(text_input){
  text_input %>% 
    str_remove_all(pattern='\n') %>% # remove quebras de linha desnecessárias
    str_remove_all(pattern= '[:punct:](\\S+[:space:])?') %>% #remove as @ citadas. 
    str_remove_all(pattern= 'http\\S+') %>%  # remove as URLs
    toupper() # Design Decision: para ficar parecido com um louco.
}

treat <- function(dados){
  dados %>% 
    select(created_at, text, favorite_count, reply_count) %>% # seleciona as colunas desejadas.
    mutate(text = clean_tweets(text)) %>% # passa os tweets pela função clean.
    filter(nchar(text) > 20) #remove linhas com menos de 20 caracteres
}

dados <- tweets_carluxo %>% 
  treat() # RUN.

Acima temos duas funções: clean_tweets e treat, a primeira será utilizada dentro da segunda, e é composta por 3 chamadas à função stringr::str_remove_all(), e uma à função toupper(). As 3 chamadas iniciais são ajustes no texto necessários para remover elementos indesejados.

  • /'\n'/ é a expressão regular para que sejam removidas o conjunto de caracteres que indica quebra de linha.

  • /[:punct:](\\S+[:space:])?/ remove todo agrupamento textual composto por pontuação-palavra-espaço, foi o mecanismo que recorri para remover as @’s que o Carluxo citou. Elas não serão úteis para nós, além disso, fazer um tweet com essas @s seria notificar alguém sobre nosso bot, o que não é nosso objetivo.

  • /http\\S+/ remove links iniciados ou qualquer palavra que tenha http em seu inicio, como não existem muitas

Por fim, temos…

  • toupper o que essa função, que faz parte do R-base (ou seja, ela já vem instalado no seu R), faz é transformar todos os caracteres em maíusculos.

Nós utilizaremos o artifício da capitalização por dois motivos, um de design, e outro técnico. Sobre o design, o motivo é que Carlos é conhecido por gritar, então deixar em maíusculo dá um tom cômico às publicações. Já a justificativa técnica é que nosso algoritmo funciona com probabilidade de uma determinada palavra aparecer, contudo em diversos momentos podemos ter a mesma palavra com capitalização diferenciada. Não é desejável para nós que o computador diferencia ‘canalha’ de ‘Canalha’, visto que as duas palavras tem o mesmo significado, então padronizamo-as com a capitalização total. Utilizar tolower() para caracteres todas minúsculos também é uma opção.

Em algumas situações, palavras com capitalização diferente podem significar diferentes coisas, por exemplo, "A Grande Sambista, a Marrom" se refere à gloriosa cantora Alcione, por outro lado, a sentença o carro marrom se refere a algum carro feio. Contudo, isso é a minoria dos casos, e caso aconteça em algum momento e o leitor queira fazer a diferenciação, é possível utilizar ‘Part-of-Speech Tagging’ para fazer a separação.

Então, temos uma segunda função, a treat, o que ela faz é receber nosso data.frame com os resultados da API, selecionar por meio da função dplyr::select() as colunas desejadas, e então, utilizando dplyr::mutate(), aliado com nossa função clean_tweets() trata a coluna text. Então, em um terceiro - e último - passo, filtramos os Tweets por tamanho, removendo todos com menos de 20 caracteres de extensão, essa remoção é necessária para que sejam removidas as linhas vazias ou com muito pouco conteúdo, dessa forma agilizamos nosso modelo.

Por fim, passo os nossos dados pelas funções que criamos, e… Voilá!

created_attextfavorite_countreply_count
2020-05-01 07:26:19SE VI ESSA MULHER NA CASA DO MEU PAI 4 VEZES FOI MUITO O QUE TODOS VIAM ERA ALGUÉM MAIS PREOCUPADA EM SE FILMAR EM EVENTOS PÚBLICOS AO LADO DE JAIR BOLSONARO PARA SE PROMOVER E DEPOIS FAZER O QUE FEZ COMO MUITOS MINHA IRMÃÉ MUITO CARA DE PAU MEU DEUS6634NA
2020-05-01 07:14:04TUDO ENGATADO UM NO OUTROACUSE OS DO QUE VOCÊ É554NA
2020-05-01 02:31:39O VALE CINZENTO DA RAZÃO VAI MUITO ALÉM DOS CALÇAS ENCRAVADAS12669NA
2020-05-01 02:23:03PRUDÊNCIA E SOFISTICAÇÃOCÊ CURTE22640NA
2020-04-30 17:24:11MAIS EXEMPLOS DE ATUAÇÕES DO NOS ÚLTIMOS DIAS 23825NA
2020-04-30 17:19:06MAIS EXEMPLOS DE ATUAÇÕES DO NOS ÚLTIMOS DIAS 18820NA
model_it <- dados %>%  #remove pontuação
  pull(text) %>% #puxa a coluna que contem o texto dos tweets.
  str_split(' ') %>%  #quebra a coluna, cada palavra vira um elemento de lista
  unlist %>% # remove os caracteres das listas que a função anterior retorna
  na.omit() # remove caracteres missing (NA), não deve haver nenhum, mas só por precaução.

Passando agora os preparativos finais, é importante que o objeto inserido na função que calculará o modelo seja:

  1. Grande: Quando maior o vetor inserido em nosso modelo, maiores serão as frases que ele conseguirá formular.

  2. Tokenizado: O objeto inserido deve ser um vetor de caracteres, onde cada elemento é uma palavra.

Para que atendamos a essas obrigações precisamos que façamos alterações em nossos dados, que estão em formado de dataframe. Para resolver esse problema, nós extraímos somente a coluna text do dataframe - em formato de vetor - utilizando a função dplyr::pull(). A partir disso, quebramos nosso vetor em uma lista em que cada item é composto é um vetor de caracteres, nestes, cada palavra (usamos o espaço como separador) é um elemento. Contudo, listas não são o formato desejado, e utilizamos a função unlist() para remover os vetores, transformando-os em um large character vector. Por fim, removemos nos NA potenciais usando a na.omit().

tic()
model <- markovchainFit(model_it[1:20000]) # fita o modelo
toc()
## 303.786 sec elapsed

Para atingir o ponto que queriamos, então, fitamos o modelo utilizando o markovchain::markovchainFit(), aqui utilizaremos somente as primeiras 20000 palavras do nosso vetor, isso será feito por limitações computacionais. Quanto maior o vetor inserido, mais tempo a computação do modelo levará. Acima temos o tempo que levou para rodar no meu computador. O argumento n da função indica quantas palavras devem ser gerados pelo modelo.

for(i in 1:3){
  markovchainSequence(model$estimate, n = 20) %>% 
    paste(collapse= ' ') %>% 
    print()
}
## [1] "INVASÕES DE DESENVOLVIMENTO COMERCIAL COM ENTREGA DO SENADO PODEM FAZER ISSO E MUNICÍPIOS DE CONSERVAÇÃO NO MERCADO INTERNACIONAL O RISCO"
## [1] " ALGUÉM ACHA MESMO QUE DEMOCRACIA AMEAÇA NÓS JAMAIS PENSEI QUE HOJE SABEMOS A BASE SEJA DERRUBÁÀ FORÇA AGORA 24"
## [1] "ÀS DROGAS NO BRASIL O TEOR DA TRIBUTAÇÃO DO GOVERNO BOLSONARO ACABOU O MÊS DE 0NA COMPARAÇÃO COM NOME JAIR"

Pronto, nosso modelo está pronto, acima temos 3 exemplos de palavras geradas por ele.

Iremos então fazer uma função para dar ajustes finais no tweet que nosso modelo gerar, e então fazer o Tweet caso este atenda ao critério para publicação que é: possuir menos de 280 caracteres (é o limite atual de caracteres de um tweet).

tuita_ai = function(model_here){
    
  tweet = markovchainSequence(model_here$estimate, n = 30) %>% 
    paste(collapse = ' ') %>% 
    str_remove('[:space:](A|O)$')
  
  if(nchar(tweet)<=280){
    post_tweet(tweet)
  } else {
    message('Tweet maior que o desejado, tentando novamente, aguarde...')
    tuita_ai(model_here)
  }
}

tuita_ai(model) #Foi!
## your tweet has been posted!

A função acima faz o seguinte:

  1. Cria uma função chamada tuita_ai que receberá o nosso modelo, e fará:
  • Cria dentro da função uma variável tweet - ela não ficará visivel em seu ambiente, para entender esse comportamento confira o capitulo 6.4 do manual Advanced R. Essa variável recebe o resultado de uma computação de nosso modelo, e é colapsada em uma string. São removidos então os caracteres ‘A’ ou ‘O’ caso estejam sozinhos no fim da frase.

A remoção de ‘O’ ou ‘A’ foi feita pois percebi durante a escrita desse post que é normal a ocorrencia dessas letras, sozinhas, no fim da frase.

  • Irá conferir a quantidade de caracteres da string gerada, caso seja menor ou igual a 280, publicará o tweet.

  • Caso a string não seja maior ou igual, portanto maior, que 280 caracteres, irá exibir a mensagem de nova tentativa, e executará novamente nossa função por meio de uma recursão. Peço cuidado aqui, colocar um valor muito grande no n da função markovchainSequence() pode gerar uma recursão infinita, o que é indesejado.

Enfim, rodamos nosso modelo, se tudo deu certo, ele deve exibir uma mensagem de sucesso, e seu tweet foi feito!

É isso, essa postagem ficou um pouco longo, mas queria mostrar que é possível nos divertirmos pouco esforço. :)

Abraços, até a proxima!