Padrões de Pesquisa Reprodutível com R + GNU Make
Esta é uma skill do Claude. Carregue com /skill r-gnumake-orchestrator no Claude para acessar padrões para construir pipelines de análise de dados reproduzíveis.
Um guia para orquestrar pipelines de análise em R usando GNU Make com workflows baseados em arquivos e um contrato de interface de script padronizado.
Visão Geral
Esta skill captura padrões genéricos para análise de dados reproduzível:
- Scripts como funções puras: Aceitam argumentos, leem entradas, escrevem uma saída e saem
- Makefiles com alvos de arquivo: Reconstroem apenas quando dependências mudam
- Configuração compartilhada: Uma única fonte da verdade para todos os scripts
- Ecossistema Tidyverse: Pacotes R modernos e consistentes
- Documentação roxygen2: Cada função reutilizável é documentada
- Sem impressão de saída: Resultados residem apenas em arquivos de dados versionados
Esses padrões escalam de análises exploratórias pequenas a grandes compêndios de pesquisa com dezenas de estágios e milhares de artefatos gerados.
Convenções de Scripts R e Makefile
Padrão de Script R
Todo script de pipeline define .args condicionalmente: padrões interativos se executado no console, commandArgs() se chamado pelo Make.
# Define argumentos: padrões para interativo, commandArgs quando chamado pelo Make
.args <- if (interactive()) c(
"data/default1.rds",
"data/default2.rds",
"R/shared_inputs.R",
"output/default.csv"
) else commandArgs(trailingOnly = TRUE)
# Extrai de .args por posição
input1 <- .args[1]
input2 <- .args[2]
helper <- .args[3]
output <- .args[4]
source(helper) # carrega COUNTRY, SEED, OUTPUT_DIR, etc.
# Agora: leia entradas, compute, escreva saída, saia
data <- read_rds(input1)
results <- process(data)
write_csv(results, output)
No Make: Rscript $^ $@ passa todas as pré-requisitos em ordem, depois o alvo.
Interativamente: Rscript R/myscript.R usa os padrões de .args (nenhum argumento necessário).
Padrão de Makefile
Use alvos de arquivo (não regras phony). Reconstrua apenas quando as dependências mudarem.
# Criação de diretório (apenas ordem, não dispara reconstruções)
output output/figures output/tables:
mkdir -p $@
# Entrada única → saída única
output/processed.rds: data/raw.csv R/process.R R/shared_inputs.R | output
Rscript $^ $@
# Múltiplas entradas → saída única
output/scores.csv: output/model.rds data/test.rds R/score.R R/shared_inputs.R | output
Rscript $^ $@
# Alvos phony apenas por conveniência (não rastreados pelo Make)
.PHONY: all
all: output/scores.csv
Sintaxe chave:
$^= todas as pré-requisitos em ordem$@= nome do arquivo alvo| output= pré-requisito apenas de ordem (diretório criado, mas timestamp ignorado)
Configuração Compartilhada
Um arquivo contém todo valor que dois ou mais scripts precisam. Sem duplicação entre scripts.
# R/shared_inputs.R
COUNTRY <- "Afghanistan"
HORIZON <- as.integer(Sys.getenv("HORIZON", "1"))
SEED <- 42
OUTPUT_DIR <- file.path("output", paste0("horizon_", HORIZON))
VALIDATION_YEARS <- 2017:2023
# Todo script carrega isso, nunca redefine
source("R/shared_inputs.R")
Organização de Arquivos
project/
├── Makefile # Regras de orquestração (alvos de arquivo)
├── R/
│ ├── shared_inputs.R # Configuração (carregado por todos os scripts)
│ ├── process_data.R # Scripts do pipeline
│ ├── fit_model.R # (um trabalho por script)
│ ├── score_model.R
│ ├── helpers.R # Funções reutilizáveis (sem prefixo de estágio)
│ ├── models.R # Documentado com roxygen2
│ └── utils.R
├── data/
│ ├── raw/ # Dados fonte
│ ├── processed/ # Gerados (gitignored ou rastreados por DVC)
│ └── features/
├── output/
│ ├── figures/ # Versionados (para o manuscrito renderizar no CI)
│ ├── tables/ # Arquivos .csv com todos os números
│ ├── models/ # Objetos de modelo ajustados (.rds)
│ └── predictions/ # .csv com saídas do modelo
└── paper/
├── paper.qmd # Manuscrito (lê output/*.csv)
└── bibliography.bib
Prefixos de estágio ordenam por ordem de execução:
process_*.R→fit_*.R→score_*.R→visualise_*.R
Arquivos de funções não têm prefixo para que ferramentas de documentação possam distingui-los de scripts de pipeline.
Regras de Padrão do Make
Use regras de padrão (%) para alvos repetitivos. Defina uma vez, aplique a muitos.
# Padrão: processa cada arquivo de entrada
INPUTS := file1 file2 file3
# Regra genérica: transforma qualquer %.csv em output/processed_%.rds
output/processed_%.rds: input/%.csv R/process.R R/shared_inputs.R | output
Rscript $^ $@
# Alvo de conveniência: todos os arquivos processados
all_processed: $(patsubst input/%.csv, output/processed_%.rds, $(wildcard input/*.csv))
# Sobrescrita para caso especial: file1 precisa de entrada diferente
output/processed_file1.rds: input/file1_v2.csv R/process.R R/shared_inputs.R | output
Rscript $^ $@
Uma saída por regra: todo script escreve exatamente um arquivo, então toda regra tem exatamente um alvo. Se um estágio precisar de dois arquivos, escreva dois scripts e duas regras (veja "Uma Saída por Script" em Padrões Comuns).
Organização de Funções
Scripts de Pipeline
Um trabalho por script, e uma saída por script. Cada estágio lê o que o estágio anterior escreveu.
# R/fit_model.R
# Propósito: Ajustar um modelo a dados preparados
# Entradas: data/prepared.rds, R/shared_inputs.R
# Saída: output/model.rds
.args <- if (interactive()) c(
"data/prepared.rds",
"R/shared_inputs.R",
"output/model.rds"
) else commandArgs(trailingOnly = TRUE)
data_file <- .args[1]
helper <- .args[2]
model_file <- .args[3]
source(helper)
data <- read_rds(data_file)
model <- fit_model(data, seed = SEED)
write_rds(model, model_file)
Qualquer outra coisa derivada das mesmas entradas, como um resumo dos dados de treinamento, tem seu próprio script e sua própria saída:
# R/summarise_training.R
# Propósito: Resumir os dados usados para ajustar o modelo
# Entradas: data/prepared.rds, R/shared_inputs.R
# Saída: output/model_meta.csv
.args <- if (interactive()) c(
"data/prepared.rds",
"R/shared_inputs.R",
"output/model_meta.csv"
) else commandArgs(trailingOnly = TRUE)
data_file <- .args[1]
helper <- .args[2]
meta_file <- .args[3]
source(helper)
data <- read_rds(data_file)
write_csv(
tibble(
n_training = nrow(data),
n_features = ncol(data) - 1
),
meta_file
)
Arquivos de Funções
Funções reutilizáveis vão em arquivos separados (sem prefixo de estágio). Documente com roxygen2.
# R/models.R
#' Ajusta um modelo a dados
#'
#' Treina uma floresta aleatória usando parâmetros especificados.
#'
#' @details
#' Suposições sobre as entradas:
#' - dados têm coluna `outcome` (binária: 0/1) e todos os preditores são numéricos
#' - seed é um inteiro para reprodutibilidade
#'
#' @param data Um tibble com colunas de resposta e preditores.
#' @param seed Inteiro para geração de números aleatórios.
#' @param mtry Inteiro; número de variáveis a amostrar por divisão.
#'
#' @return Um objeto de modelo ajustado (classe ranger).
#'
#' @export
fit_model <- function(data, seed = 42, mtry = floor(sqrt(ncol(data) - 1))) {
model <- ranger(outcome ~ ., data = data, seed = seed, mtry = mtry)
model
}
Execute make docs para regenerar man/ e NAMESPACE a partir dos blocos roxygen.
Configuração Compartilhada
Local central para parâmetros de todo o estudo. Todo script carrega.
# R/shared_inputs.R
# Design do estudo
COUNTRY <- "Country_Name"
SEED <- 42
ANALYSIS_YEARS <- 2017:2023
# Caminhos (computados, não hardcoded)
DATA_DIR <- "data"
OUTPUT_DIR <- "output"
HORIZON <- as.integer(Sys.getenv("HORIZON", "1"))
# Hiperparâmetros do modelo
RF_MTRY <- NULL # sqrt(p)
RF_NTREES <- 500
# Não defina mais nada aqui.
# Valores específicos de estágio permanecem no script do estágio.
Diretrizes de Estilo
Stack Tidyverse
Padronize nestes pacotes:
library(tidyverse) # core: dplyr, tidyr, ggplot2, readr, purrr
library(fs) # operações de sistema de arquivos (dir_create, file_exists)
library(cli) # sinalização de erro (cli_abort, cli_warn)
library(here) # caminhos relativos (here("R", "script.R"))
library(scales) # formatação de gráficos
library(yardstick) # métricas de modelo
Nunca use:
- Subsetting base ou
aggregate()(usedplyr::filter(),dplyr::summarise()) map_dfr()(usemap()+list_rbind())group_by()+ungroup()(use argumento.by =)gather(),spread()(usepivot_longer(),pivot_wider())%>%(use pipe nativo|>)function(x)(use lambda\(x))- Funções obsoletas (verifique o badge de ciclo de vida na página de ajuda)
Formatação de Código
Use o formatador air (impõe linhas de 80 caracteres, indentação de 2 espaços):
air format R/ # formata diretório inteiro
air format R/myscript.R # formata um arquivo
Configurações em air.toml na raiz do repositório (versionado, não pessoal).
Comentários
Comente por que o código faz algo, não o que (apenas onde a lógica não é óbvia pela leitura do código). Evite comentários que apenas repetem o código.
# Ruim: repete o que o código obviamente faz
# Ajusta uma floresta aleatória com hiperparâmetros padrão
model <- ranger(outcome ~ ., data = data)
# Bom: explica por que fizemos essa escolha
# Floresta aleatória em vez de CART aqui porque precisamos de intervalos de confiança de regressão quantílica
model <- ranger(outcome ~ ., data = data)
# Ruim: óbvio pelo código
# Filtra linhas onde ano está em ANALYSIS_YEARS
data <- data |> filter(year %in% ANALYSIS_YEARS)
# Bom: explica a lógica não óbvia
# Exclui ano de 2015 devido a cobertura de vigilância incompleta (veja Métodos)
data <- data |> filter(year >= 2016)
# Sem comentário necessário: o código é autoexplicativo
data <- data |>
select(district, year, outcome) |>
filter(!is.na(outcome))
Escreva comentários na seção de Métodos do manuscrito ao explicar uma escolha metodológica; use comentários de código apenas quando a decisão for inesperada ou fora do padrão.
Sem Impressão de Saída
Resultados residem apenas em arquivos CSV, RDS e PNG versionados.
Nenhum message(), print(), cat(), ou sprintf() para saída informacional.
Exceções: cli_abort() e cli_warn() (apenas fluxo de controle).
# Ruim
message(sprintf("Processed %d rows", nrow(data)))
cat("Model trained\n")
# Bom (escreva em arquivo em vez disso)
meta <- tibble(n_rows = nrow(data))
write_csv(meta, "output/metadata.csv")
O manuscrito lê output/metadata.csv no momento da renderização, então todo número aparece em arquivos versionados.
Fluxo de Validação
Ao escrever um novo script, teste progressivamente antes de adicionar regras ao Make.
Passo 1: Teste Interativamente (com padrões)
# Funciona sem argumentos?
Rscript R/myscript.R
# Verifique se os arquivos de saída foram criados
ls -lh output/
Passo 2: Teste com Argumentos (imita o Make)
# Passe argumentos como o Make faria
Rscript R/myscript.R \
data/raw.csv \
R/shared_inputs.R \
output/result.rds
# Compare com a saída do Passo 1
Passo 3: Adicione Regra do Make e Teste
# No Makefile:
output/result.rds: data/raw.csv R/myscript.R R/shared_inputs.R | output
Rscript $^ $@
# Teste
make output/result.rds
# Verifique: modificar dados deve disparar reconstrução
touch data/raw.csv
make output/result.rds # Deve re-executar
# Verifique: modificar output/result.rds (não dependência) não dispara reconstrução
touch output/result.rds
make output/result.rds # Deve dizer "up to date"
Passo 4: Simulação
make --dry-run output/result.rds # Mostra o que seria executado sem executar
Exemplo: Pipeline Simples de 3 Estágios
Makefile
.PHONY: all
all: output/summary.csv
output:
mkdir -p $@
output/processed.rds: data/raw.csv R/process.R R/shared_inputs.R | output
Rscript $^ $@
output/model.rds: output/processed.rds R/fit.R R/shared_inputs.R | output
Rscript $^ $@
output/summary.csv: output/model.rds output/processed.rds R/score.R R/shared_inputs.R | output
Rscript $^ $@
R/process.R
.args <- if (interactive()) c(
"data/raw.csv",
"R/shared_inputs.R",
"output/processed.rds"
) else commandArgs(trailingOnly = TRUE)
raw_file <- .args[1]
helper <- .args[2]
output <- .args[3]
source(helper)
data <- read_csv(raw_file) |>
filter(year %in% ANALYSIS_YEARS) |>
mutate(across(everything(), as.numeric))
write_rds(data, output)
R/fit.R
.args <- if (interactive()) c(
"output/processed.rds",
"R/shared_inputs.R",
"output/model.rds"
) else commandArgs(trailingOnly = TRUE)
data_file <- .args[1]
helper <- .args[2]
output <- .args[3]
source(helper)
data <- read_rds(data_file)
model <- ranger(outcome ~ ., data = data, seed = SEED)
write_rds(model, output)
R/score.R
.args <- if (interactive()) c(
"output/model.rds",
"output/processed.rds",
"R/shared_inputs.R",
"output/summary.csv"
) else commandArgs(trailingOnly = TRUE)
model_file <- .args[1]
data_file <- .args[2]
helper <- .args[3]
output <- .args[4]
source(helper)
model <- read_rds(model_file)
data <- read_rds(data_file)
predictions <- predict(model, data)$predictions
scores <- tibble(
auc = AUC::auc(data$outcome, predictions),
n = nrow(data)
)
write_csv(scores, output)
Execute
make # Constrói tudo em ordem (process → fit → score)
make output/model.rds --always-make # Força reconstrução apenas desta etapa
make --dry-run # Mostra o que seria executado
Lista de Verificação para Novos Scripts
Ao escrever um novo estágio de pipeline:
- Script aceita argumentos em ordem: entradas → helper → saída
- Script inclui padrões
.argspara teste interativo - Script carrega
R/shared_inputs.R(nunca define valores de todo o estudo) - Script lê suas entradas, computa e escreve exatamente um arquivo de saída
- Nenhum
print(),message()oucat()(usecli_abort(),cli_warn()para erros apenas) - Todos os resultados escritos em arquivos CSV/RDS/PNG versionados
- Dependências listadas corretamente no Makefile (em ordem)
- Pré-requisitos de diretório apenas de ordem usam sintaxe
| - Testado interativamente, depois com argumentos, depois via Make
- Regra do Makefile usa sintaxe
$^e$@ - Código formatado com
air format R/scriptname.R - Funções reutilizáveis extraídas para arquivos separados