Reproducibilidade de Código com o R
Reproducibilidade de C?digo com o R published at the "Open Code Community"
Uma pesquisa baseada em dados é reproduzível quando outros cientistas conseguem replicar os seus resultados. O benefício para a ciência é óbvio: diferentes pesquisadores podem repetir experimentos computacionais e testar novas hipóteses com base em resultados já estabelecidos. Claramente, políticas de reproducibilidade são uma condição necessária para promovermos a ciência perante uma audiência cada vez mais crítica do trabalho acadêmico. Como confiar em resultados os quais não permitem replicação? Seria a palavra do pesquisador suficiente? Claro que não..
Apesar de parecer uma premissa simples, a prática de reproducibilidade em pesquisa baseada em dados é complexa. No mundo ideal, o mesmo script de pesquisa – código de computador que produz gráficos e tabelas – deveria sempre produzir os mesmos resultados. Na prática, entretanto, todo código de programação possui diversas dependências: o sistema operacional (Windows/Mac/Linux), a plataforma estatística utilizada (R/Python/Stata/..) e versões das funções específicas de cada plataforma. Uma maneira estruturada de entender o problema é imaginar um grid com todas estas possíveis combinações de plataforma computacional, versões de software e funções para a pesquisa. Uma pesquisa é reproduzível se, dada a mesma entrada de dados, a saída do programa (gráfico/tabela) é equivalente em todas combinações do grid.
Como professor e pesquisador, já vivenciei situações onde a reproducibilidade de código não persiste, com alguns casos mais graves que outros. Aprendi a lidar com a reproducibilidade na marra, com erros e acertos. Assim, neste pequeno artigo irei mostrar o que a plataforma R tem para oferecer em termos de reproducibilidade e compartilhar o que aprendi no processo.
Reproducibilidade com o R
Organização de pastas e arquivos
O primeiro e mais simples movimento para melhorar a reproducibilidade de código é organizar os seus arquivos. Pode parecer bobagem, mas uma básica organização facilita muito o processo de execução de códigos antigos. Quem nunca baixou um código estilo miojo da internet?
Por exemplo, imagine você abrir uma pasta com um projeto antigo e se deparar com o seguinte cenário:
## Loading required package: fs
## /tmp/Rtmp3rqa7g/Minha-Pesquisa
## ├── data
## │ ├── dados-atualizados.csv
## │ └── dados-finais.csv
## ├── script-artigo-final2--REVISADO.R
## ├── script-download-dados.R
## ├── script-download-dados2.R
## ├── tabela-final1.xlsx
## └── tabela-final2-antes-de-filtrar.xlsx
Neste caso temos três scripts do R, uma pasta com dados e dois arquivos .csv. Os nomes dos arquivos também não ajudam. Afinal, qual arquivo representa os dados utilizados na pesquisa publicada? O que parece óbvio no momento de edição do projeto, ou uma semana depois, não será tão óbvio em seis meses. Se não é óbvio para o próprio autor, imagine para outras pessoas.
Como sugestão, e deixo claro que a forma de organização é intrinsicamente gosto pessoal, entendo que a melhor estrutura é aquela mais simples e óbvia. Um exemplo para o projeto anterior seria a seguinte organização de arquivos e pastas:
## /tmp/Rtmp3rqa7g/Minha-Pesquisa-oganizada
## ├── 01-import-clean-data.R
## ├── 02-make-tables.R
## ├── data
## │ └── dados-pesquisa-2021-05-01.rds
## ├── fcts
## │ └── fcts-models.R
## ├── figs
## │ ├── p1-variable-over-time.png
## │ └── p2-distribution-plot.png
## └── tabs
## ├── tab01-summary-stats.tex
## └── tab02-ols-model.tex
Daqui a um ano ou mais, caso eu ou outra pessoa retorne a este projeto,
a estrutura de arquivos e pastas permite o fácil e rápido entendimento
do código: dois script numerados e sequenciados
(01-import-clean-data.R
e 02-make-tables.R
), um de dados com data
dados-pesquisa-2021-05-01.rds
e pastas para as saídas em tabelas
(tabs/
) e figuras (figs/
).
Assim, como critérios pessoais, utilizo as seguintes regras:
- Na pasta raiz do projeto, somente código R pode existir (todo o resto, arquivos de figura, tabelas, dados, devem ir em pasta própria);
- Se os scripts possuem ordem de execução, use numeração no nome, de forma a ficar claro o sequenciamento (ex. 01-import-data.R, 02-clean-data.R, 03-make-tables.R);
- Se dados podem mudar com o tempo, usar uma data na nomeação do arquivo (ex: dados-tesouro-direto_2021-01-05.csv);
- Figuras e tabelas devem ser nomeadas na ordem em que aparecem no artigo ou relatório (ex. p1-grafico-tempo.png, p2-histograma.png, tab1-descritiva.tex, etc)
Pacote renv
Pacote renv
tem uma missão específica: controlar e gerenciar pacotes
de projetos. Sua estrutura e funcionamento é muito semelhante ao
Virtual Enviromnent
(venv) do Python. A
grosso modo, a partir de um diretório de trabalho, registramos os
pacotes utilizados – incluindo versões – em um arquivo local chamado
renv.lock
. Após isso, podemos copiar o código para outra máquina e
restaurar todos pacotes e suas versões específicas com um simples
comando. Isto é, reproduzimos o ambiente específico do projeto em termos
de pacotes e versões do R.
O primeiro passo para usar renv
é definir uma pasta de trabalho onde
código R e arquivos dados irão viver. Ao mudar o diretório para esta
pasta com setwd
ou usando a ferramenta de projetos do RStudio, a qual
já muda o diretório automaticamente para onde o arquivo de projeto
existe, basta inicializar o renv
com comando renv::init()
. O que
este comando irá fazer é ler os scripts do R existentes na raiz e
subdiretórios da pasta atual, localizar no código as chamadas a
library/require
ou ::
, e registrar todas as dependências de
pacotes. Por exemplo, se fizeres uma chamada a library(dplyr)
em um
código de R na pasta (ou subpasta) de trabalho, o renv::init()
irá
identificar esta dependência.
Após inicializarmos o renv
no projeto, basta registrar as dependências
– pacotes e suas versões – com o comando renv::snapshot()
, e pronto!
Agora, quando copiarmos o código para outro computador, basta usar
renv::restore()
para instalar e usar todas as dependências do projeto.
Resumindo:
- Abra o projeto e use
renv::init()
para inicializar orenv
; - Use
renv::snapshot()
para registar todos pacotes e versões; - Use
renv::install()
para instalar novos pacotes (funçãoinstall.packages()
é sobrescrito porrenv
e funciona da mesma forma); - Use
renv::update()
para atualizar todos pacotes, incluindo pacotes instalados do Github.
Para mais detalhes sobre renv
, veja o site
oficial. Eu uso o
renv
em todos os meus projetos e só tenho elogios:
- O overhead de tempo para instalação e configuração é mínimo;
- A pasta de trabalho tem um aumento pequeno de tamanho pois todos pacotes são na verdade links simbólicos (e não arquivos locais);
- Facilita muito o uso de pacotes em computadores diferentes. Ao copiar a pasta, basta digitar um comando para sincronizar a máquina com todos os pacotes do projeto;
Pacote checkpoint
Pacote checkpoint
é uma iniciativa da
Microsoft
para aumentar a reproducibilidade de códigos em R. A grande sacada do
pacote é utilizar o tempo, e não projetos, para definir as versões dos
pacotes. Podes, por exemplo, usar todas versões dos pacotes na data
2020-01-05
. O pacote acessa um repositório atualizado com as linhas de
tempo das versões e instala apenas aquelas disponíveis na data de
referência.
Assim como para o renv
, o uso do checkpoint
é bastante simples,
basta carregar o pacote no topo do script e chamar a principal função
com uma data, como em:
library(checkpoint)
checkpoint("2020-01-01")
# resto do código aqui
O que o checkpoint
irá fazer é definir um repositório local para os
pacotes, buscar pacotes utilizados no código, e instalar todas versões
disponíveis naquela data em particular, incluindo as versões das
dependências. A partir disso, o resto do código acessará o repositório
local do checkpoint
para buscar todos os pacotes utilizados nos
scripts.
Pessoalmente, acho a forma de utilizar o checkpoint
bem interessante
porém um pouco gananciosa no sentido de tentar oferecer uma solução
mágica para o problema de reproducibilidade. Na prática, ao usar o
checkpoint
em meus projetos, vejo os seguintes problemas:
- A instalação de todos pacotes na inicialização do
checkpoint
acaba exigindo certo tempo de processamento pois todas as dependências devem que ser reinstaladas; - A primeira data do repositório de pacotes é
'2014-09-17'
, o que pode não ser suficiente para projetos mais antigos; - Dependências externas ao R, como software instalado via apt do linux, não são resolvidas. Assim, no futuro, sem a garantia de que dependências externas serão sempre disponíveis, a reproducibilidade fica comprometida;
- Adiciona um custo de armazenamento significativo. Ao reinstalar pacotes, um projeto mínimo pode ter tamanho maior que 100 MB, simplesmente pelo uso de um reposítório customizado.
Para mais detalhes, veja a página do projeto no Github.
Conteinarização – docker
Hoje em dia é impossível falar de reproducibilidade sem mencionar a tecnologia docker. Primeiramente pensada para a execução de códigos em produção – sistemas ativos e importantes–, o docker permite a criação de uma imagem de um sistema operacional dentro do seu computador. Na minha opinião, atualmente o docker é o ápice do que se consegue em termos de reproducibilidade computacional, justificando o seu uso sistêmico na indústria.
Por exemplo, digamos que voce tenha um código em R desenvolvido no sistema operacional Ubuntu 18.04, e versões específicas de software via apt e pacotes do R. Assim, podes criar uma imagem para recriar este ambiente computacional no seu computador, instalar todas as dependências, incluindo software por apt-get e pacotes do R e, por fim, executar o código dentro da imagem e exportar resultados para arquivos Excel ou .csv. Assim, mesmo que estejas usando Windows 10, podes rodar o código R no sistema Ubuntu 18.04.
O uso do docker com o R vai muito além deste artigo, exigindo
conhecimento sobre arquiteturas e códigos em linha de comando. Em
resumo, utilizar docker resume-se a 1) baixar uma imagem de um sistema
operacional, 2) escrever um Dockerfile
com todos os passos de
preparação do sistema para execução de scripts e 3) executar o código.
Para quem quiser conhecer mais, um tutorial muito bom está disponível
aqui. Um vídeo
ilustrativo está disponível no
Youtube.
Conclusão
Reproducibilidade no R não é uma tarefa fácil, mas os pacotes existentes
hoje ajudam bastante. Particularmente, uso o renv
em todos os projetos
que faço e estou muito satisfeito. Entendo que o renv
fornece o
equilíbrio entre reproducibilidade e inconveniência, não atrapalhando o
desenvolvimento do código. Quem quiser saber mais sobre
reproducibilidade no R, o próprio CRAN tem uma página
especial
sobre o tópico. Lá encontrarás diversos outros pacotes voltados a
reproducibilidade e não citados aqui.
Please, cite this work:
Perlin, Marcelo; Reichert, Marcos Henrique; Mussoi, Lucas (2021), “Reproducibilidade de Código com o R published at the “Open Code Community””, Mendeley Data, V1, doi: 10.17632/scpb9gnsgm.1