Category

Data Science

Hackeando o R: gerando gráficos de modo interativo com o esquisse

By | Hackeando o R

No Hackeando o R de hoje, vamos apresentar o pacote esquisse, que facilita a geração de gráficos interativos no R. A principal funcionalidade do pacote é o add-in que ele adiciona ao R, que permite gerar gráficos de modo intuitivo e sem olhar diretamente para o código, sendo uma boa ferramenta para trabalhos rápidos. Após instalar o pacote, podemos acessar suas ferramentas no RStudio:

Vamos então carregar alguns dados para trabalhar dentro da ferramenta. Para o exemplo, utilizaremos 4 das principais variáveis reportadas pelo Boletim FOCUS.

library(rbcb)
library(tidyverse)

dados = get_annual_market_expectations(c('PIB Total', 'IPCA', 
'Taxa de câmbio', 
'Meta para taxa over-selic'
),
start_date = '2019-01-01') %>%
replace_na(replace = list(indic_detail = 'Média'))

dados$indic = ifelse(dados$indic == 'Taxa de câmbio', 'Taxa de Câmbio', 
dados$indic)

dados = dados %>% filter(reference_year == '2021' & base == 0 & 
indic_detail %in% c('Média', 'Fim do ano') & 
indic %in% c('IPCA', 'Meta para taxa over-selic',
'PIB Total', 'Taxa de Câmbio') & 
date > '2020-06-01')

Ao abrir o add-in, iremos selecionar os dados a serem utilizados:

Dentro da ferramenta, teremos então todas as colunas do dataframe, e diversas opções do ggplot2. Abaixo, colocamos a data no eixo x, a expectativa média no eixo y, e separamos os dados pela variável. É importante notar que os dados já foram transformados no formato tall, como fazemos em gráficos de múltiplas linhas normalmente.

Podemos personalizar diversas configurações, como adicionar título, mudar coordenadas, e alterar o layout. Após isso, podemos salvar o gráfico localmente. Outra opção é salvá-lo diretamente em um slide de PowerPoint. Isso pode ser feito tanto diretamente no add-in, como para objetos do tipo ggplot que você já possui localmente.



Análise das Atas do COPOM com text mining

By | Data Science, Política Monetária

Introdução

Mineração de texto, ou text mining, é um tópico muito interessante, pois há um potencial enorme de aplicações para obtenção de insights através dessa técnica envolvendo análise textual. Com a finalidade de demonstrar seu uso, neste post faremos uma breve e introdutória análise das atas do Comitê de Política Monetária - COPOM usando text mining com o auxílio do pacote tidytext.

As atas do COPOM são um caminho natural para qualquer economista em busca de uma fonte de dados para exercitar o text mining, já que a autoridade monetária realiza ajustes (mudanças) no texto a cada reunião, realizada a cada 45 dias. As atas são disponibilizadas publicamente neste link em arquivos PDFs (atualmente há 238 reuniões realizadas). Inicialmente vamos pegar o arquivo da última reunião publicada e importar os dados para o R, fazendo tratamentos e posterior análise. Por fim, vamos coletar os dados de todo o histórico de atas para refazer a análise de forma a incorporar o componente temporal.

Um ponto importante é que utilizaremos as versões em inglês das atas, apesar de o COPOM disponibilizar também em português. Isso se deve ao fato de que as ferramentas aqui utilizadas, para aplicar a técnica de text mining, funcionarem melhor com textos na língua inglesa.

Pacotes

Os pacotes utilizados neste exercício podem ser instalados/carregados com o gerenciador pacman, todos provenientes do CRAN:


# Instalar/carregar pacotes
if(!require("pacman")) install.packages("pacman")
pacman::p_load(
"tidytext",
"pdftools",
"stopwords",
"textdata",
"dplyr",
"tidyr",
"lubridate",
"magrittr",
"knitr",
"ggplot2",
"ggthemes",
"jsonlite",
"purrr",
"stringr",
"scales",
"forcats"
)

Text mining de uma ata do COPOM

Vamos importar a última ata do COPOM para o R. Primeiro criamos um objeto para armazenar o link para o arquivo e, na sequência, usamos a função pdf_text do pacote pdftools para ler cada página do arquivo PDF e transformar os dados em um vetor de caracteres de tamanho igual ao número de páginas.

# URL da última ata do COPOM
www <- "https://www.bcb.gov.br/content/copom/copomminutes/MINUTES%20238.pdf"
# Ler arquivo PDF convertendo para caracter
raw_copom_last <- pdftools::pdf_text(www)

Tratamento de dados

Um primeiro olhar sobre os dados mostrará que há uma série de caracteres especiais "\n" e "\r" que indicam quebras de linhas. Portanto, vamos tratar de forma a obter um objeto tibble com uma coluna (text) com os dados do texto de cada página da ata, outra coluna (meeting) indicando o mês da reunião e a última coluna (page) que informará a página referente ao texto.

# Tratamento de dados
copom_last_clean <- dplyr::tibble(
text = unlist(strsplit(raw_copom_last, "\r"))
) %>% 
dplyr::mutate(
meeting = "May 2021", 
page = dplyr::row_number(),
text = gsub("\n", "", text)
)

Text mining

Agora estamos com os dados quase prontos para uma análise! O que faremos agora é começar a aplicar algumas das técnicas de text mining provenientes do livro Text Mining with R escrito pela Julia Silge e David Robinson.

O primeiro passo é "tokenizar" nossos dados, de forma a obter "unidades do texto" que servirão para a análise. Isso facilitará para realizarmos a contagem de palavras frequentes, filtrar palavras em específico, etc. O processo de "tokenizar" é definido no livro como:

"A token is a meaningful unit of text, most often a word, that we are interested in using for further analysis, and tokenization is the process of splitting text into tokens."

Para utilizar a técnica, usamos a função unnest_tokens do pacote tidytext e na sequência realizamos a contagem das palavras encontradas:

# Text mining - Criar tokens
copom_last <- copom_last_clean %>% 
tidytext::unnest_tokens(word, text)
# Contar palavras
copom_last %>%
dplyr::count(word, sort = TRUE) %>% 
dplyr::slice_head(n = 6) %>% 
knitr::kable()


Como podemos observar, as palavras mais frequentes são palavras comuns como "the", "of", "in", etc. que não servirão muito para nossa análise. Essas palavras são chamadas de "stop words" no mundo do text mining. Vamos fazer uma tratativa para remover essas palavras usando como base o objeto stop_words proveniente do pacote tidytext e, antes, removemos também números que foram apontados como "palavras":

copom_last_sw <- copom_last %>%
# Remover palavras comuns (stop words)
dplyr::anti_join(stop_words)%>%
# Remover números
dplyr::mutate(word = gsub("[^A-Za-z ]", "", word)) %>%
# Contar palavras
dplyr::count(word, sort = TRUE) %>% 
dplyr::filter(word != "")
copom_last_sw %>% 
dplyr::slice_head(n = 6) %>% 
knitr::kable()

Agora sim temos os dados prontos para uma análise de sentimento!

Análise de sentimento

Nosso foco agora é usar ferramentas de análise para tirar informação desses dados, ou seja, queremos saber o que as palavras das atas do COPOM podem indicar com base na tentativa de apontarmos um "score" para cada uma delas, usando datasets e bibliotecas de "sentimento do texto" do tidytext para isso.

Vamos ver quais são as palavras negativas e positivas usadas com mais frequência. A função get_sentiments do tidytext fará isso pra gente, bastando apontar um lexicon, que pode ser "bing", "afinn", "loughran" ou "nrc".

# Obter análise de sentimento das palavras com base em uma biblioteca
copom_last_sw %>%
dplyr::inner_join(tidytext::get_sentiments("bing")) %>% 
dplyr::slice_head(n = 10) %>% 
knitr::kable()

Como resultado, "risks" é a palavra negativa que aparece mais vezes (13 no total) nessa ata do COPOM, e "recovery", uma palavra positiva, é usada 4 vezes no total. Um segundo olhar sobre esse resultado pode levantar uma suspeita, pois "recovery" é apontada como uma palavra positiva (recuperação), no entanto, é razoável supor que pode estar também associada com uma situação anterior negativa. Isso é um possível problema da análise que o leitor pode investigar.
Agora vamos explorar a análise graficamente:

# Análise de sentimento final da ata de Maio/2021
copom_sentiment <- copom_last %>%
dplyr::inner_join(tidytext::get_sentiments("bing")) %>%
dplyr::count(meeting, page, sentiment) %>%
tidyr::pivot_wider(
id_cols = c(meeting, page),
names_from = sentiment, 
values_from = n,
values_fill = 0
) %>%
dplyr::mutate(sentiment = positive - negative)
# Gerar gráfico
copom_sentiment %>% 
ggplot2::ggplot(ggplot2::aes(page, sentiment, fill = sentiment > 0)) +
ggplot2::geom_col(show.legend = FALSE) +
ggplot2::scale_fill_manual(values = c("#b22200", "#282f6b")) +
ggplot2::labs(
x = "Página da ata",
y = "Sentimento",
title = "Análise de sentimento da Ata do COPOM - Maio/2021",
subtitle = "Bing lexicon",
caption = paste0("Elaboração: analisemacro.com.br\nDados: ", www)
)

O gráfico conta uma história interessante. O texto começou negativo na página 3 (sobre atualização de conjuntura, cenário e riscos), mas depois foi "neutro" na página 4 (sentimento positivo - negativo = 0) - onde é discutido a condução da política monetária e -, por fim, na última página o texto fica positivo com a decisão da reunião do COPOM.

Vale enfatizar que a primeira página, referente a capa, não é considerada na análise. Já a página 2, da contracapa, possui igual número de palavras negativas e positivas.

Text mining com todas as atas do COPOM

Agora vamos aplicar a técnica apresentada acima para um conjunto maior de dados, desta vez vamos pegar todas as atas disponíveis no site do Banco Central do Brasil - BCB através deste link.

Vamos expandir nossa análise capturando o texto de cada Ata do COPOM desde sua 42ª reunião, totalizando 198 atas para nossa análise. Faremos a comparação da frequência relativa de palavras e tópicos e veremos como o sentimento (conforme explorado acima) varia entre os relatórios.

Dados

Infelizmente, os links para as atas em PDF seguem um padrão irregular, mas felizmente para você, preparei um web-scrapping que automatizará o processo de captura dos links e dados. O código a seguir coletará os arquivos PDF e os deixará prontos para a mineração com o tidytext.

# URL para página com JSON dos links das atas
www_all <- "https://www.bcb.gov.br/api/servico/sitebcb/copomminutes/ultimas?quantidade=2000&filtro="
# Raspagem de dados
raw_copom <- jsonlite::fromJSON(www_all)[["conteudo"]] %>% 
dplyr::as_tibble() %>% 
dplyr::select(meeting = "Titulo", url = "Url") %>% 
dplyr::mutate(url = paste0("https://www.bcb.gov.br", url)) %>% 
dplyr::mutate(text = purrr::map(url, pdftools::pdf_text))
# Tratamento de dados
copom_clean <- raw_copom %>% 
tidyr::unnest(text) %>% 
dplyr::filter(!meeting == "Changes in Copom meetings") %>% 
dplyr::group_by(meeting) %>%
dplyr::mutate(
page = dplyr::row_number(),
text = strsplit(text, "\r") %>% gsub("\n", "", .),
meeting = stringr::str_sub(meeting, 1, 3) %>% 
stringr::str_remove("[:alpha:]") %>% 
as.numeric()
) %>%
dplyr::ungroup() %>% 
tidyr::unnest(text) %>% 
dplyr::arrange(meeting)

Estatística básica dos dados

Vamos ver o que conseguimos obter calculando algumas estatísticas básicas dos textos.

Número de palavras por ata

# Frequência de palavras por ata
copom_words <- copom_clean %>%
tidytext::unnest_tokens(word, text) %>%
dplyr::count(meeting, word, sort = TRUE) %>%
dplyr::ungroup()
# Total de palavras por ata
total_words <- copom_words %>% 
dplyr::group_by(meeting) %>% 
dplyr::summarize(total = sum(n))
# Gerar gráfico
total_words %>% 
ggplot2::ggplot(ggplot2::aes(x = meeting, y = total)) +
ggplot2::geom_line(color = "#282f6b", size = 0.8)+
ggplot2::geom_point(
shape = 21, 
fill = "white", 
color = "#282f6b", 
size = 1.6, 
stroke = 1.1
) +
ggplot2::scale_y_continuous(labels = scales::number_format()) +
ggplot2::labs(
x = "Reunião (nº da ata)", 
y = "Número de palavras",
title = "Número de palavras nas atas do COPOM",
subtitle = "42ª até 238ª reunião",
caption = "Elaboração: analisemacro.com.br\nDados: BCB"
)

Percebe-se algumas mudanças ao longo do tempo, especialmente entre as reuniões número 180 e 181, onde houve remoção considerável de seções do comunicado na gestão Tombini. No mesmo sentido, de redução do total de palavras por comunicado, o início da gestão de Illan Goldfajn marcou mudança no layout e estrutura das seções da ata, alterações essas que permanecem em vigor até hoje.

A redução do tamanho dos comunicados, de forma geral, é certamente um ponto interessante que merece investigação em se tratando de qualidade de comunicação da política monetária.

Sobre o que os diretores discutiram nas reuniões?

Vamos compilar uma lista das palavras usadas com maior frequência em cada ata. Como antes, vamos omitir as "stop words".

# Palavras por ata
copom_text <- copom_clean %>% 
dplyr::select(meeting, page, text) %>%
tidytext::unnest_tokens(word, text)
# Gerar gráfico
copom_text %>% 
dplyr::mutate(word = gsub("[^A-Za-z ]", "", word)) %>%
dplyr::filter(word != "") %>%
dplyr::anti_join(stop_words) %>%
dplyr::group_by(meeting) %>%
dplyr::count(word, sort = TRUE) %>% 
dplyr::mutate(rank = dplyr::row_number()) %>%
dplyr::ungroup() %>% 
dplyr::arrange(rank, meeting) %>%
dplyr::filter(rank < 9, meeting > 230) %>% 
ggplot2::ggplot(ggplot2::aes(y = n, x = forcats::fct_reorder(word, n))) +
ggplot2::geom_col(fill = "#282f6b") +
ggplot2::facet_wrap(~meeting, scales = "free", ncol = 4) +
ggplot2::coord_flip() +
ggplot2::labs(
x = "",
y = "",
title = "Palavras mais frequentes nas atas do COPOM",
subtitle = "Excluídas palavras comuns (stop words) e números.",
caption = "Elaboração: analisemacro.com.br\nDados: BCB"
)


Como esperado, muitas discussões sobre inflação. Vamos tentar encontrar algo mais informativo com esses dados.

Conforme Silge e Robinson, podemos usar a função bind_tf_idf para juntar a frequência do termo (tf) e a frequência inversa do documento (idf) ao nosso conjunto de dados. Essa estatística diminuirá o peso em palavras muito comuns e aumentará o peso em palavras que só aparecem em algumas atas. Em essência, extrairemos o que há de especial em cada ata. As atas do COPOM sempre falarão muito sobre inflação e juros, e a estatística "tf-idf" pode nos dizer algo sobre o que é diferente em cada ata.

Também limparemos alguns termos adicionais que o pdftools captou (abreviações e palavras estranhas ou fragmentadas), aumentando nossa lista de "stop words".

# Stop words personalizadas
custom_stop_words <- dplyr::bind_rows(
dplyr::tibble(
word = c(
tolower(month.abb), 
tolower(month.name),
"one","two","three","four","five","six","seven","eight","nine","ten",
"eleven","twelve", "wkh", "ri", "lq", "month", "wr", "dqg",
"hdu", "jurzwk", "zlwk", "zlwk", "hfhpehu", "dqxdu", "kh", "sulfh", "dv",
"kh", "prqwk", "hdu", "shulrg", "dv", "jurzwk", "wkdw", "zdv", "iru", "dw",
"wkdw", "jrrgv", "xqh", "eloolrq", "eloolrq", "iluvw", "dq", "frqvxphu", 
"prqwk", "udwh", "sulo", "rq", "txduwhu", "vhfwru", "pandemic", 
"dffxpxodwhg", "hg", "kdyh", "sdqghg", "sulfhv", "rq", "sdqvlrq", 
"percent", "forvhg", "frpsduhg", "lqgh", "ryhpehu", "wklv", "kdv", "prqwkv",
"bcbgovbr", "banco", "head"
), 
lexicon = c("custom")
),
stop_words
)
# Palavras por ata
copom_text_refined <- copom_text %>% 
dplyr::mutate(word = gsub("[^A-Za-z ]", "", word)) %>%
dplyr::filter(word != "") %>%
dplyr::group_by(meeting) %>% 
dplyr::count(word, sort = TRUE) %>% 
tidytext::bind_tf_idf(word, meeting, n) %>% 
dplyr::arrange(desc(tf_idf))
# Gerar gráfico
copom_text_refined %>% 
dplyr::anti_join(custom_stop_words, by = "word") %>%
mutate(word = factor(word, levels = rev(unique(word)))) %>% 
dplyr::group_by(meeting) %>%
dplyr::mutate(id = dplyr::row_number()) %>%
dplyr::ungroup() %>% 
dplyr::filter(id < 9, meeting > 230) %>% 
ggplot2::ggplot(ggplot2::aes(y = tf_idf, x = word, fill = meeting)) +
ggplot2::geom_col(show.legend = FALSE) +
ggplot2::facet_wrap(~meeting, scales = "free", ncol = 4) +
ggplot2::coord_flip() +
ggplot2::labs(
x = "",
y = "tf-idf",
title = "Palavras mais distintas nas atas do COPOM",
subtitle = "Estatística tf-idf do tidytext",
caption = "Elaboração: analisemacro.com.br\nDados: BCB"
) +
ggplot2::theme(axis.text.x = ggplot2::element_text(angle = 45, hjust = 1))

Esse gráfico já mostram uma história interessante dessa amostra que escolhemos. Podemos observar preocupações com o BREXIT, reformas e agenda econômica, o surgimento do termo "covid" a partir da ata nº 231 e subsequente adoção do forward guidance e, por fim, também como destaque, a mudança da política para uma "normalização parcial" (termos "partial" e "normalization").

Comparando o sentimento entre as atas
Como o sentimento variou entre as atas? Vamos usar a abordagem que usamos no início para a ata de maio/2021 e aplicá-la a cada ata.

# Análise de sentimento das atas
copom_sentiment_all <- copom_text %>%
dplyr::anti_join(stop_words) %>%
dplyr::inner_join(tidytext::get_sentiments("bing")) %>%
dplyr::count(meeting, page, sentiment) %>%
tidyr::pivot_wider(
id_cols = c(meeting, page),
names_from = sentiment, 
values_from = n,
values_fill = 0
) %>%
dplyr::mutate(sentiment = positive - negative)
# Gerar gráfico
copom_sentiment_all %>% 
dplyr::filter(meeting > 230) %>% 
ggplot2::ggplot(ggplot2::aes(page, sentiment, fill = sentiment > 0)) +
ggplot2::geom_col(show.legend = FALSE) +
ggplot2::scale_fill_manual(values = c("#b22200", "#282f6b")) +
ggplot2::facet_wrap(~meeting, ncol = 4, scales = "free_x") +
ggplot2::annotate("segment", x = -Inf, xend = Inf, y = -Inf, yend = -Inf)+
ggplot2::annotate("segment", x = -Inf, xend = -Inf, y = -Inf, yend = Inf) +
ggplot2::labs(
x = "Página da ata",
y = "Sentimento",
title = "Análise de sentimento das Ata do COPOM",
subtitle = "Bing lexicon, amostra das últimas 8 atas",
caption = "Elaboração: analisemacro.com.br\nDados: BCB"
)

O resultado mostra que o sentimento mudou consideravelmente no texto das atas da amostra das últimas 8 reuniões, com forte predominância "negativa" desde o advento da pandemia da Covid-19.

Conclusão

Isso é tudo por hoje. Essas técnicas são muito interessantes e promissoras, merecendo exploração aprofundada. Certamente o exercício aqui apresentado possui falhas e pontos de melhoria, mas serve como uma introdução ao tema. Espero que goste, caro leitor!

Possui interesse no tema de política monetária? A Análise Macro disponibiliza o curso de Teoria da Política Monetária dentro da temática de Central Banking. Aproveite!

_____________

Hackeando o R: extraindo dados de múltiplos arquivos no R

By | Hackeando o R

No Hackeando o R de hoje, vamos mostrar como criar dataframes organizados quando nossos dados estão quebrados em diversas planilhas. Esse é um problema comum ao tratar de dados que são padronizados porém possuem diversas fontes, e, na falta de uma API ou outro sistema de extração automatizada, acabamos tendo que baixar uma pasta com cada um dos arquivos. Abaixo, como exemplo motivador, vamos supor que queremos extrair a localização de cada uma das estações meteorológicas automatizadas no Brasil na malha do INMET. Os dados disponíveis no site são as observações de cada ano, logo poderíamos supor que para termos todas as estações, bastaria pegar o ano mais recente de dados (2020). Apesar disso, vamos utilizar múltiplos anos, para mostrar que também podemos trabalhar com dados que pertencem a múltiplas subpastas. Em uma pasta qualquer, as planilhas têm nomes identificadores:

Além disso, como mencionado anteriormente, os dados em cada planilha são padronizados, logo as transformações aplicadas em uma também funcionarão em todas as outras. Com isso, vamos ver o formato de uma delas:

Com isso, nosso procedimento vai ser o seguinte:
- Listar todos os nomes dos arquivos relevantes e os subdiretórios onde se encontram;
- Gerar uma função que extrai os dados de um arquivo qualquer;
- Aplicar a função sobre cada um dos arquivos e agregar os resultados.

O primeiro passo é o mais simples. Primeiramente, você deve garantir que está trabalhando no diretório onde se encontram os arquivos usando setwd(), ou colocando o seu script na mesma pasta e configurando no RStudio com
Session -> Set working directory -> To source file location
Após isso, podemos listar os dados com a função list.files(). Como queremos dados que estão em subpastas, utilizamos o argumento recursive=TRUE, que indica para o R que ele deve abrir as subpastas e pegar os nomes dos arquivos que estão lá. A pasta de trabalho também possui outros arquivos, porém apenas as planilhas começam com "INMET" no nome, logo utilizamos uma expressão regular na função com o argumento pattern. A expressão abaixo começa com ^, indicando que é o começo da string, e INMET, logo são listadas as strings que começam com INMET.

 

arquivos = list.files(pattern = '^INMET', recursive = TRUE)

Temos então 1794 arquivos distintos. Agora, vamos utilizar o tidyverse para extraírmos os dados que queremos. Nesse exemplo, eles serão as 7 primeiras linhas de cada planilha. O código abaixo gera uma função que abre um arquivo utilizando codificação latina (para termos acentos), transforma as 2 primeiras colunas em linhas, limpa os nomes, e transforma as variáveis numéricas que eram strings devido ao uso de vírgula para decimais em números do tipo double.

library(tidyverse)

extrator = function(dados){
read_csv2(dados, locale = locale(encoding = 'latin1'), n_max=7, col_names = FALSE) %>%
pivot_wider(names_from = X1, values_from = X2) %>%
rename('Região' = `REGIÃO:`, 'UF' = `UF:`, 'Código' = `CODIGO (WMO):`,
'Latitude' = `LATITUDE:`, 'Longitude' = `LONGITUDE:`,
'Estação' = `ESTAÇÃO:`, 'Altitude' = `ALTITUDE:`) %>%
mutate(across(Latitude:Altitude, function(x) as.numeric(gsub(',', '.', x))))
}

De posse da função, nos resta apenas aplicá-la sobre cada um dos arquivos. Para fazer isso, utilizaremos a função map() do pacote purrr, incluída no tidyverse. Como queremos gerar um único dataframe com cada um dos dados, e nossa função gera objetos com colunas de mesmo nome, iremos utilizar a função map_dfr, que aplica nossa função extrator() sobre cada objeto da lista arquivos, e os agrega linha por linha. É importante notar que a função será demorada, logo é bom testar com um conjunto pequeno antes de rodar sobre a lista completa. Ademais, como podemos ter repetição entre as estações, terminamos nosso código com a função unique(), que remove linhas iguais.

agregado = map_dfr(arquivos, extrator) %>% unique()

Abaixo, um excerto do resultado final:

Como automatizar scripts de R usando o GitHub Actions

By | Data Science

Se você tem um script de R para executar uma determinada rotina em uma frequência predeterminada, talvez seja interessante investir em economia de tempo automatizando essa tarefa. Nesse tutorial demonstramos como utilizar o GitHub Actions para rodar scripts de R automaticamente, se livrando de qualquer trabalho manual e repetitivo.

São diversos os contextos e aplicações nos quais a automatização de um script de R pode ser útil, dentre os principais exemplos destaca-se:

  • Relatórios dinâmicos com R Markdown
  • Atualização de dashboards
  • Extração e atualização de uma base de dados
  • Web scrapping de uma página
  • Rotinas de robustez contra erros em pacotes

Muitos desses exemplos, e a depender do contexto, requerem que o usuário de R execute o script em uma frequência mensal, semanal, diária ou até mesmo de 1 em 1 hora. Dessa forma, a automatização não é apenas interessante, mas vital para um bom fluxo de trabalho.

Vamos pegar um desses exemplos como demonstração nesse tutorial: faremos um web-scrapping da página de criptomoedas do site TradingView para obter dados de cotações, volume, retornos %, etc. e, adicionalmente, criaremos uma dashboard básica com esses dados para demonstrar uma possibilidade de uso. Por fim, automatizaremos esse processo de modo que a dashboard seja atualizada de hora em hora.

Pré-requisitos

Antes de começar, para esse tutorial deve-se cumprir os seguintes pré-requisitos (pule essa etapa se você já os cumpre):

Se houverem dificuldades com o Git, principalmente se for a primeira vez, recomendo fortemente dar uma lida no Happy Git and GitHub for the useR, que é um tutorial de como usar o Git e GitHub integrado ao R/RStudio.

Criar um pacote

O primeiro passo é criar um pacote R que vai conter nosso script na forma de uma função. Mas não se assuste, é mais fácil do que parece pois, felizmente, a comunidade de R já criou diversas ferramentas para facilitar esse processo. Ao final, utilizaremos a função para a rotina de web-scrapping dos dados de criptomoedas, da mesma forma que a maioria dos pacotes de coleta de dados funcionam.

Aqui utilizamos o pacote usethis para criar um pacote básico. Basta passar para função create_package o caminho (path) de onde se deseja armazenar os arquivos que serão gerados, no nosso caso ficará dentro de uma pasta chamada cryptoscrap.

usethis::create_package("cryptoscrap")

Uma nova sessão do R será aberta com um R Project já pré configurado e com os arquivos mínimos do nosso pacote.

Em sequência, abrimos o arquivo DESCRIPTION recém criado e preenchemos algumas informações básicas como título do pacote, descrição, informações de autor, etc. e, opcionalmente pode ser utilizado uma licença como a MIT (neste caso pode ser usado a função usethis::use_mit_license()). Para saber mais sobre esse preenchimento básico, assim como outras informações sobre desenvolvimento de pacotes, recomendo conferir o livro do Hadley Wickham e Jenny Bryan intitulado R Packages.

Finalizado esse procedimento inicial, podemos prosseguir para o script de R.

Escrever um script de R

Conforme mencionado, faremos um web-scrapping de uma tabela com dados de criptomoedas provenientes do TradingView. Utilizaremos apenas dois pacotes nesta etapa, o rvest e o purrr, e colocaremos a rotina dentro de uma função que será solicitada na dashboard mais adiante.

Para começar a criar a função usamos novamente o pacote usethis, gerando um novo arquivo de scripts dentro da pasta R do nosso projeto (entre aspas o nome do arquivo .R que será gerado):

usethis::use_r("crypt_scrap")

No novo arquivo .R criado escrevemos nossa função:

crypt_scrap <- function() {

# Crytocurrency page URL
crypto_url <- rvest::read_html(
"https://www.tradingview.com/markets/cryptocurrencies/prices-all/"
)

# Get raw data from URL
crypto_raw <- rvest::html_nodes(crypto_url, css = "table")

# Convert from list to tibble
crypto_table <- rvest::html_table(crypto_raw)
crypto_table <- purrr::pluck(crypto_table, 1)
crypto_table <- purrr::set_names(
crypto_table,
c(
"Name", 
"Mkt Cap",
"FD Mkt Cap",
"Last", 
"Avail. Coins", 
"Total Coins",
"Traded Vol", 
"Chg %"
)
)

# Return tibble
return(crypto_table)
}

Em sequência, devemos adicionar as dependências do nosso pacote, ou seja, estes dois pacotes que estamos utilizando na nossa função. Isso faz com que, ao carregar localmente o pacote que estamos criando por exemplo, estes dois pacotes externos também sejam carregados para a sessão do R ao utilizar a função crypt_scrap que criamos.

Para adicionar as dependências é muito simples:

usethis::use_package("rvest")
usethis::use_package("purrr")

Agora já temos um rascunho de pacote pronto para ser testado! Para testar, é importante verificar se a função está funcionando localmente. Outro teste que pode ser feito é através do pacote devtools, que oferece ferramentas para desenvolvimento de pacotes com uma série de parametrizações úteis que indicam se o pacote e suas funções seguem as regras e recomendações gerais do CRAN. Para isso basta rodar:

devtools::check()

Ao final dos testes é indicado se houve errors, warnings ou notes. Se estiver tudo ok, o que foi o caso, podemos prosseguir.

Criar um repositório no GitHub

Agora vamos criar um repositório no GitHub para hospedar nosso pacote com os arquivos. Tudo que precisa ser feito é:

  1. Estar logado no GitHub
  2. Clicar no botão New
  3. Definir um nome do repositório (aqui usamos crypto_scrap)
  4. Clicar em Create repository

Opcionalmente pode ser incluído uma descrição, similar ao que preenchemos no arquivo DESCRIPTION do nosso pacote.

Em seguida o GitHub criará um link para o repositório criado, direcionando para esta página, onde aparecerão alguns comandos em Git que podemos utilizar para integrar nosso projeto local com o repositório. Tudo que precisamos fazer é copiar o código que aparece na sugestão intitulada "…or create a new repository on the command line" e colar no Terminal do RStudio para fazer a configuração necessária. No nosso exemplo executamos o código abaixo:

echo "# crypto_scrap" >> README.md
git init
git add README.md
git commit -m "first commit"
git branch -M main
git remote add origin https://github.com/schoulten/crypto_scrap.git
git push -u origin main

Com efeito, nossa pasta de pacote local estará configurada como um repositório Git e assim já podemos subir nossos arquivos (fazer o push) para o GitHub. Para isso, executamos no Terminal:

git add .
git commit -m "adding package"
git push

Ao voltarmos na página do GitHub, dando uma atualizada na página, todos os nossos arquivos locais também estarão lá disponíveis.

Criar uma dashboard

Agora vamos deixar o GitHub em stand by para voltar ao R e criar uma dashboard com os dados das criptomoedas. Nesse exemplo vamos criar uma dashboard estática contendo apenas uma tabela com os dados (bem básico), e utilizamos o pacote flexdashboard para tal. Os códigos em .rmd para gerar a dashboard podem ser conferidos nesse link. O resultado é esse:

Criado o nosso "produto final", podemos partir para a etapa final de automatização.

Usar o renv para gerenciar dependências

Em seguida, fazemos um passo adicional em nosso projeto que é criar um arquivo renv.lock que conterá uma série de configurações de gerenciamento de dependências do nosso projeto/pacote. O que basicamente isso significa é que será tirada uma "foto" do estado atual de configurações, pacotes, dependências que estamos utilizando, para que o projeto seja reprodutível em outro ambiente, como o GitHub Actions.

Esse assunto é bastante complexo e não é o foco aqui se aprofundar no tema, de modo que tudo que precisa ser feito é executar no Console:

renv::init()
renv::snapshot()

Agendar tarefa com o GitHub Actions

Como pode ter sido observado, criamos um fluxo de trabalho aqui:

  • Temos um pacote com uma função para extrair dados de criptomoedas
  • Usamos essa função em uma dashboard que gera uma tabela com os dados

Agora o que queremos é delegar esse fluxo de trabalho para que o GitHub Actions faça isso automaticamente para nós, em uma periodicidade de 60 em 60 minutos, já que os dados oferecem essa tempestividade.

Portanto, o que faremos na prática é:

  • Criar um arquivo (workflow) que agenda uma tarefa no GitHub Actions
  • Especificar um intervalo de agendamento da tarefa
  • Configurar o ambiente R no GH Actions
  • Executar script de web-scrapping + atualização da dashboard
  • Salvar (commit e push) os resultados

Importante: a indentação do código é importante aqui, se tiver dificuldade em identificar os espaçamentos corretos, confira neste link como ficou o resultado final dessa parte do nosso código.

Criar arquivo de workflow do GitHub Actions

Primeiro precisamos criar o arquivo de workflow, que conterá toda a rotina do que queremos que o GitHub Actions execute. Ele deve estar dentro de uma pasta .github/workflows a partir da raiz do projeto que estamos trabalhando e ter uma extensão .yaml. Em nosso exemplo, ficou assim:

├── .github
│ ├── workflows
│ ├── update.yaml

O nome do arquivo update.yaml pode ser qualquer um.

Especificar um intervalo de agendamento da tarefa

Utilizaremos um agendador de tarefas denominado "CRON" para executar periodicamente nossos códigos.

A parte mais importante para se compreender é como especificar o intervalo de tempo com o qual estamos trabalhando utilizando uma expressão: o script deve ser executado a cada 5 minutos? De hora em hora? Diariamente? Mensalmente? Para cada uma dessas possibilidades há uma expressão específica de configuração e neste link podemos encontrar alguns exemplos dessas expressões para tomar como base.

Em nosso caso, como queremos a execução de 60 em 60 minutos, usamos a expressão "0 * * * *". No arquivo de workflow a sintaxe é conforme abaixo, logo nas primeiras linhas:

on:
push:
branches:
- main
- master
schedule:
- cron: "0 * * * *"

Configurar o ambiente R no GH Actions

Em seguida, vamos configurar uma sessão de R no servidor do GitHub Actions, já que, basicamente, o GH Actions é nada mais do que um computador localizado em algum lugar do mundo que o GitHub nos disponibiliza para executarmos algo nele, em nosso caso são scripts de R. A abordagem aqui utilizada se baseia em alguns templates fornecidos pela equipe do r-lib.

Resumidamente, esta parte do arquivo de workflow:

- Especifica o tipo de sistema operacional a ser usado (usaremos Windows)
- Define as variáveis de ambiente R e configura a autenticação de acesso ao repositório GitHub
- Instala o R
- Instala/restaura os pacotes necessários

name: Dashboard update
jobs:
Dashboard-update:
runs-on: windows-latest
env:
GITHUB_PAT: ${{ secrets.GITHUB_TOKEN }}
steps:
- uses: actions/checkout@v2
- uses: r-lib/actions/setup-r@v1
- name: Cache packages
uses: actions/cache@v2
with:
path: ~\AppData\Local\renv
key: ${{ runner.os }}-renv-${{ hashFiles('**/renv.lock') }}
restore-keys: |
${{ runner.os }}-renv-
- name: Restore packages
run: |
if (!requireNamespace("renv", quietly = TRUE)) install.packages("renv")
renv::restore()
shell: Rscript {0}
- name: Install workflow dependencies
run: |
install.packages(c("rcmdcheck", "sessioninfo", "devtools"))
shell: Rscript {0}

Executar script de web-scrapping + atualização da dashboard

Com o R configurado, agora executa-se os scripts que escrevemos. No nosso exemplo, a função de web-scrapping do pacote que criamos será chamada dentro do arquivo .rmd que gera a dashboard.

- name: Install pandoc
run: |
choco install pandoc
- name: Render Rmarkdown/Update data
run: |
devtools::load_all()
rmarkdown::render("index.Rmd")
shell: Rscript {0}

Salvar (commit e push) os resultados

Até aqui o workflow terá gerado uma nova dashboard com os dados de criptomoedas atualizada, mas tudo isso (esses arquivos) estarão no servidor do GitHub Actions. Portanto, precisamos, por fim, fazer o commit & push dessas atualizações para o nosso repositório do GitHub. Isso é feito conforme abaixo:

- name: Commit and push
run: |
git config --local user.email "actions@github.com"
git config --local user.name "GitHub Actions"
git add .
git commit -m "Dashboard update"
git push

- name: Session info
run: |
options(width = 500)
pkgs <- installed.packages()[, "Package"]
sessioninfo::session_info(pkgs, include_base = TRUE)
shell: Rscript {0}

Salvar resultados no repositório

Ok, o trabalho duro já passou, agora só falta fazermos um novo check em nosso pacote, verificar se está tudo ok e salvo e subir as últimas modificações para o repositório do GitHub.

Primeiro o check no Console do RStudio:

devtools::check()

Apareceram 2 notes e para corrigir adicionamos as seguintes linhas no arquivo .Rbuildignore:

index.Rmd
index.html
^\.github$

Agora podemos prosseguimos com o commit & push, no Terminal do RStudio:

git add .
git commit -m "create pkg, dash and workflow"
git push

Ao visitarmos novamente a página do nosso repositório criado no site do GitHub, veremos todas as modificações realizadas.  E pronto!

Além disso, no botão "Actions" do repositório aparecerá o nosso workflow desenhado para executar toda essa rotina que trabalhamos aqui, de hora em hora. Portanto, estamos por fim "livres" de trabalho manual e o nosso produto final, uma dashboard, está totalmente automatizada. Legal, não?!

Recomendo esperar até 24 horas para verificar se o workflow está funcionando, pois o GitHub pode ter um delay para começar a executar toda a rotina definida.

O resultado final desse exemplo disponibilizei neste repositório e a dashboard pode ser acessada por este link.

Ressalvas

Esse tutorial introdutório resumiu demasiadamente diversos pontos que merecem atenção, de modo que, possivelmente, não cobre alguns possíveis obstáculos que podem surgir ao longo do desenvolvimento de um projeto desses. Recomendamos utilizar exaustivamente as documentações dos pacotes e ferramentas aqui utilizadas, de modo a entender mais profundamente o que está sendo feito.

Por fim, eu adoraria saber como como foi a experiência individual ao tentar reproduzir esse tutorial. Sinta-se livre para mandar um feedback, caro leitor(a).

 

Análise estatística espacial no R (parte 2)

By | Data Science, Economia

Semana passada mostramos como estimar a dependência espacial de variáveis no R. Hoje, iremos continuar a falar sobre econometria espacial, apresentando como rodar modelos de regressão que levem em consideração o espaço. Para isso, utilizaremos um exemplo simples em que estimamos a relação  da taxa de homicídios com o PIB per capita.

O argumento mais importante para se estimar modelos de econometria espacial é se a suposição de independência entre as observações não é mais válida. Assim, características de um local podem influenciar nos resultados de outro.


library(geobr)
library(sidrar)
library(spdep)

library(stargazer)

set.ZeroPolicyOption(TRUE)

Utilizaremos basicamente o mesmo processo de extração de dados do post anterior, com a utilização de microrregiões como unidade geográfica. A diferença será a inclusão da variável de com os dados de taxa de homicídio.

#Dados de PIB
pib = get_sidra(5938,
variable = 37, 
geo = "MicroRegion")
#Dados de população
pop= get_sidra(202,
variable = 93,
geo = "MicroRegion") %>%
filter(`Sexo (Código)` == 0 & `Situação do domicílio (Código)` == 0)

#Juntando e criando a variável de pib per capita
df <- left_join(pib, pop, by = "Microrregião Geográfica (Código)") %>%
mutate(pibpc = (Valor.x/Valor.y) - mean(Valor.x/Valor.y)) #centralizar no 0

# colocando a taxa de homicídios
tx_homic <- ipeadatar::ipeadata("THOMIC") %>%
filter(uname == "Microregions" &
date == "2018-01-01") %>%
mutate("Microrregião Geográfica (Código)" = as.character(tcode)) %>%
rename(tx_homic = value) %>%
select(tx_homic, "Microrregião Geográfica (Código)" )

df <- left_join(df, tx_homic, by = "Microrregião Geográfica (Código)")

# mapa
mapa_micro = read_micro_region(showProgress = F)
mapa_micro$code_micro = as.character(mapa_micro$code_micro)

merged = dplyr::left_join(mapa_micro, df,
by = c("code_micro" = "Microrregião Geográfica (Código)"))

 

Também iremos produzir a matriz de vizinhança da mesma forma que antes. Iremos retirar da análise a microrregião de Fernando de Noronha, pois ela não apresenta vizinhos, o que impossibilita a estimação.

nb <- poly2nb(merged, queen=TRUE)
lw <- nb2listw(nb, style="W", zero.policy=TRUE)
df = df  %>%  filter(`Microrregião Geográfica.x` != "Fernando de Noronha - PE")

 

Existem vários modelos que se encaixam nesse arcabouço, mas os dois principais são o Autorregressivo Espacial (SAR) e o de Erros Espaciais (SEM). Ambos são modificações do modelo clássico OLS:

(1)   \begin{equation*} y=X \beta+\epsilon \end{equation*}

O modelo SAR introduz um processo autoregressivo, semelhante ao que ocorre nos modelos de séries temporais:

(2)    \begin{equation*} y=\rho W y+X \beta+\epsilon \end{equation*}

Já o modelo SEM introduz o componente do processo autoregressivo no termo de erro:

(3)    \begin{equation*} \begin{gathered} y=X \beta+\epsilon \\ \epsilon=\lambda W \epsilon+u \end{gathered} \end{equation*}

Para determinar qual dos dois modelos rodar, é preciso estimar o teste do tipo multiplicador de Lagrange para avaliar se o Lambda e o W são diferentes de zero.

ols <- lm(tx_homic ~ pibpc, data = df)

lmLMtests <- lm.LMtests(ols,lw, test=c("RLMerr", "RLMlag"))
lmLMtests

Como em ambos os casos a hipótese nula foi rejeitada, o procedimento padrão é estimar o modelo que tenha retornado a estatística mais significativa. No nosso caso, estimaremos o modelo SAR.

 

sar = lagsarlm(tx_homic ~ pibpc, data = df, lw, zero.policy = TRUE)
stargazer(ols,sar)

Veja que na especificação OLS temos que a há uma relação significativa entre as duas variáveis. Quando há a inclusão do componente espacial, o parâmetro do PIB per capita deixa de ser significativo. Isso mostra que, nesse caso, estimar o modelo OLS pode gerar o chamado viés da variável omitida.

 \begin{table}[!htbp] \centering \caption{} \label{} \begin{tabular}{@{\extracolsep{5pt}}lcc} \\[-1.8ex]\hline \hline \\[-1.8ex] & \multicolumn{2}{c}{\textit{Dependent variable:}} \\ \cline{2-3} \\[-1.8ex] & \multicolumn{2}{c}{tx\_homic} \\ \\[-1.8ex] & \textit{OLS} & \textit{spatial} \\ & \textit{} & \textit{autoregressive} \\ \\[-1.8ex] & (1) & (2)\\ \hline \\[-1.8ex] pibpc & $-$0.128$^{***}$ & $-$0.017 \\ & (0.038) & (0.033) \\ & & \\ Constant & 25.402$^{***}$ & 11.914$^{***}$ \\ & (0.740) & (1.380) \\ & & \\ \hline \\[-1.8ex] Observations & 557 & 557 \\ R$^{2}$ & 0.020 & \\ Adjusted R$^{2}$ & 0.018 & \\ Log Likelihood & & $-$2,322.888 \\ $\sigma^{2}$ & & 230.258 \\ Akaike Inf. Crit. & & 4,653.776 \\ Residual Std. Error & 17.460 (df = 555) & \\ F Statistic & 11.043$^{***}$ (df = 1; 555) & \\ Wald Test & & 126.609$^{***}$ (df = 1) \\ LR Test & & 118.879$^{***}$ (df = 1) \\ \hline \hline \\[-1.8ex] \textit{Note:} & \multicolumn{2}{r}{$^{*}$p$<$0.1; $^{**}$p$<$0.05; $^{***}$p$<$0.01} \\ \end{tabular} \end{table}

 

 

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

Assinar Gratuitamente
{"cart_token":"","hash":"","cart_data":""}