Patrones de Investigación Reproducible con R + GNU Make
Esta es una habilidad de Claude. Cárgala con /skill r-gnumake-orchestrator en Claude para acceder a patrones para construir pipelines de análisis de datos reproducibles.
Una guía para orquestar pipelines de análisis en R usando GNU Make con flujos de trabajo basados en archivos destino y una interfaz de script estandarizada.
Resumen
Esta habilidad captura patrones genéricos para análisis de datos reproducibles:
- Scripts como funciones puras: Aceptan argumentos, leen entradas, escriben una salida y terminan.
- Makefiles con archivos destino: Solo se reconstruyen cuando las dependencias cambian.
- Configuración compartida: Una sola fuente de verdad para todos los scripts.
- Ecosistema Tidyverse: Paquetes de R modernos y consistentes.
- Documentación roxygen2: Cada función reutilizable está documentada.
- Sin impresión de salidas: Los resultados solo residen en archivos de datos comprometidos.
Estos patrones escalan desde análisis exploratorios pequeños hasta grandes compendios de investigación con docenas de etapas y miles de artefactos generados.
Convenciones de Scripts R y Makefile
Patrón de Script R
Cada script de pipeline define .args condicionalmente: valores predeterminados interactivos si se ejecuta en la consola, commandArgs() si se llama desde Make.
# Definir argumentos: valores predeterminados para interactivo, commandArgs cuando se llama desde Make
.args <- if (interactive()) c(
"data/default1.rds",
"data/default2.rds",
"R/shared_inputs.R",
"output/default.csv"
) else commandArgs(trailingOnly = TRUE)
# Extraer de .args por posición
input1 <- .args[1]
input2 <- .args[2]
helper <- .args[3]
output <- .args[4]
source(helper) # carga COUNTRY, SEED, OUTPUT_DIR, etc.
# Ahora: leer entradas, computar, escribir salida, terminar
data <- read_rds(input1)
results <- process(data)
write_csv(results, output)
En Make: Rscript $^ $@ pasa todos los prerrequisitos en orden, luego el destino.
Interactivamente: Rscript R/myscript.R usa los valores predeterminados de .args (no se necesitan argumentos).
Patrón de Makefile
Usa archivos destino (no reglas phony). Solo reconstruye cuando los prerrequisitos cambian.
# Creación de directorios (orden-solo, no dispara reconstrucciones)
output output/figures output/tables:
mkdir -p $@
# Una entrada → una salida
output/processed.rds: data/raw.csv R/process.R R/shared_inputs.R | output
Rscript $^ $@
# Múltiples entradas → una salida
output/scores.csv: output/model.rds data/test.rds R/score.R R/shared_inputs.R | output
Rscript $^ $@
# Destinos phony solo por conveniencia (no rastreados por Make)
.PHONY: all
all: output/scores.csv
Sintaxis clave:
$^= todos los prerrequisitos en orden$@= nombre del archivo destino| output= prerrequisito de orden-solo (directorio creado, pero la marca de tiempo se ignora)
Configuración Compartida
Un archivo contiene cada valor que necesitan dos o más scripts. Sin duplicación 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
# Cada script fuente importa esto, nunca lo redefine
source("R/shared_inputs.R")
Organización de Archivos
project/
├── Makefile # Reglas de orquestación (archivos destino)
├── R/
│ ├── shared_inputs.R # Configuración (importada por todos los scripts)
│ ├── process_data.R # Scripts del pipeline
│ ├── fit_model.R # (un trabajo por script)
│ ├── score_model.R
│ ├── helpers.R # Funciones reutilizables (sin prefijo de etapa)
│ ├── models.R # Documentado con roxygen2
│ └── utils.R
├── data/
│ ├── raw/ # Datos fuente
│ ├── processed/ # Generado (gitignorado o rastreado con DVC)
│ └── features/
├── output/
│ ├── figures/ # Comprometido (para que el manuscrito se renderice en CI)
│ ├── tables/ # Archivos .csv con todos los números
│ ├── models/ # Objetos de modelo ajustados (.rds)
│ └── predictions/ # .csv con salidas del modelo
└── paper/
├── paper.qmd # Manuscrito (lee output/*.csv)
└── bibliography.bib
Los prefijos de etapa ordenan en orden de ejecución:
process_*.R→fit_*.R→score_*.R→visualise_*.R
Los archivos de funciones no tienen prefijo para que las herramientas de documentación puedan distinguirlos de los scripts del pipeline.
Reglas de Patrón Make
Usa reglas de patrón (%) para destinos repetitivos. Define una vez, aplica a muchos.
# Patrón: procesar cada archivo de entrada
INPUTS := file1 file2 file3
# Regla genérica: transformar cualquier %.csv a output/processed_%.rds
output/processed_%.rds: input/%.csv R/process.R R/shared_inputs.R | output
Rscript $^ $@
# Destino de conveniencia: todos los archivos procesados
all_processed: $(patsubst input/%.csv, output/processed_%.rds, $(wildcard input/*.csv))
# Anulación para caso especial: file1 necesita una entrada diferente
output/processed_file1.rds: input/file1_v2.csv R/process.R R/shared_inputs.R | output
Rscript $^ $@
Una salida por regla: cada script escribe exactamente un archivo, por lo que cada regla tiene exactamente un destino. Si una etapa necesita dos archivos, escribe dos scripts y dos reglas (ver "Una Salida por Script" en Patrones Comunes).
Organización de Funciones
Scripts del Pipeline
Un trabajo por script y una salida por script. Cada etapa lee lo que la anterior escribió.
# R/fit_model.R
# Propósito: Ajustar un modelo a datos preparados
# Entradas: data/prepared.rds, R/shared_inputs.R
# Salida: 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)
Cualquier otra cosa derivada de las mismas entradas, como un resumen de los datos de entrenamiento, tiene su propio script y su propia salida:
# R/summarise_training.R
# Propósito: Resumir los datos utilizados para ajustar el modelo
# Entradas: data/prepared.rds, R/shared_inputs.R
# Salida: 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
)
Archivos de Funciones
Las funciones reutilizables van en archivos separados (sin prefijo de etapa). Documentar con roxygen2.
# R/models.R
#' Ajustar un modelo a datos
#'
#' Entrena un bosque aleatorio usando parámetros especificados.
#'
#' @details
#' Suposiciones sobre las entradas:
#' - los datos tienen la columna `outcome` (binaria: 0/1) y todos los predictores son numéricos
#' - seed es un entero para reproducibilidad
#'
#' @param data Un tibble con columnas de outcome y predictores.
#' @param seed Entero para generación de números aleatorios.
#' @param mtry Entero; número de variables a muestrear por división.
#'
#' @return Un objeto de modelo ajustado (clase ranger).
#'
#' @export
fit_model <- function(data, seed = 42, mtry = floor(sqrt(ncol(data) - 1))) {
model <- ranger(outcome ~ ., data = data, seed = seed, mtry = mtry)
model
}
Ejecuta make docs para regenerar man/ y NAMESPACE a partir de bloques roxygen.
Configuración Compartida
Ubicación central para parámetros de todo el estudio. Cada script la importa.
# R/shared_inputs.R
# Diseño del estudio
COUNTRY <- "Country_Name"
SEED <- 42
ANALYSIS_YEARS <- 2017:2023
# Rutas (calculadas, no codificadas)
DATA_DIR <- "data"
OUTPUT_DIR <- "output"
HORIZON <- as.integer(Sys.getenv("HORIZON", "1"))
# Hiperparámetros del modelo
RF_MTRY <- NULL # sqrt(p)
RF_NTREES <- 500
# No definir nada más aquí.
# Los valores específicos de cada etapa permanecen en el script de su etapa.
Guías de Estilo
Pila Tidyverse
Estandarizar en estos paquetes:
library(tidyverse) # núcleo: dplyr, tidyr, ggplot2, readr, purrr
library(fs) # operaciones del sistema de archivos (dir_create, file_exists)
library(cli) # señalización de errores (cli_abort, cli_warn)
library(here) # rutas relativas (here("R", "script.R"))
library(scales) # formato de gráficos
library(yardstick) # métricas de modelo
Nunca usar:
- Subsetting base o
aggregate()(usardplyr::filter(),dplyr::summarise()) map_dfr()(usarmap()+list_rbind())group_by()+ungroup()(usar argumento.by =)gather(),spread()(usarpivot_longer(),pivot_wider())%>%(usar pipe nativo|>)function(x)(usar lambda\(x))- Funciones obsoletas (verificar la insignia de ciclo de vida en la página de ayuda)
Formateo de Código
Usar el formateador air (impone líneas de 80 caracteres, sangría de 2 espacios):
air format R/ # formatear todo el directorio
air format R/myscript.R # formatear un archivo
Configuraciones en air.toml en la raíz del repositorio (rastreado, no personal).
Comentarios
Comentar por qué el código hace algo, no qué (solo donde la justificación no es obvia al leer el código). Evitar comentarios que solo repiten el código.
# Mal: repite lo que el código obviamente hace
# Ajustar un bosque aleatorio con hiperparámetros predeterminados
model <- ranger(outcome ~ ., data = data)
# Bien: explica por qué elegimos esto
# Bosque aleatorio en lugar de CART aquí porque necesitamos intervalos de confianza de regresión cuantílica
model <- ranger(outcome ~ ., data = data)
# Mal: obvio por el código
# Filtrar filas donde el año está en ANALYSIS_YEARS
data <- data |> filter(year %in% ANALYSIS_YEARS)
# Bien: explica el razonamiento no obvio
# Excluir el año 2015 debido a cobertura de vigilancia incompleta (ver Métodos)
data <- data |> filter(year >= 2016)
# No se necesita comentario: el código se explica por sí mismo
data <- data |>
select(district, year, outcome) |>
filter(!is.na(outcome))
Escribir comentarios en la sección de Métodos del manuscrito al explicar una elección metodológica; usar comentarios de código solo cuando la decisión es inesperada o no estándar.
Sin Impresión de Salidas
Los resultados solo residen en archivos CSV, RDS y PNG comprometidos.
No usar message(), print(), cat(), o sprintf() para salida informativa.
Excepciones: cli_abort() y cli_warn() (solo para flujo de control).
# Mal
message(sprintf("Processed %d rows", nrow(data)))
cat("Model trained\n")
# Bien (escribir a un archivo en su lugar)
meta <- tibble(n_rows = nrow(data))
write_csv(meta, "output/metadata.csv")
El manuscrito lee output/metadata.csv al renderizar, por lo que cada número aparece en archivos comprometidos.
Flujo de Trabajo de Validación
Al escribir un nuevo script, probar progresivamente antes de agregar reglas Make.
Paso 1: Probar Interactivamente (con valores predeterminados)
# ¿Funciona sin argumentos?
Rscript R/myscript.R
# Verificar que los archivos de salida se crearon
ls -lh output/
Paso 2: Probar con Argumentos (imita a Make)
# Pasar argumentos como lo haría Make
Rscript R/myscript.R \
data/raw.csv \
R/shared_inputs.R \
output/result.rds
# Comparar con la salida del Paso 1
Paso 3: Agregar Regla Make y Probar
# En Makefile:
output/result.rds: data/raw.csv R/myscript.R R/shared_inputs.R | output
Rscript $^ $@
# Probarlo
make output/result.rds
# Verificar: modificar datos debería disparar reconstrucción
touch data/raw.csv
make output/result.rds # Debería re-ejecutar
# Verificar: modificar output/result.rds (no dependencia) no dispara reconstrucción
touch output/result.rds
make output/result.rds # Debería decir "está actualizado"
Paso 4: Ejecución en Seco
make --dry-run output/result.rds # Mostrar qué se ejecutaría sin ejecutarlo
Ejemplo: Pipeline Simple de 3 Etapas
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)
Ejecutarlo
make # Construye todo en orden (process → fit → score)
make output/model.rds --always-make # Forzar reconstrucción solo de este paso
make --dry-run # Mostrar qué se ejecutaría
Lista de Verificación para Nuevos Scripts
Al escribir una nueva etapa del pipeline:
- El script acepta argumentos en orden: entradas → helper → salida
- El script incluye valores predeterminados de
.argspara pruebas interactivas - El script importa
R/shared_inputs.R(nunca define valores de todo el estudio) - El script lee sus entradas, computa y escribe exactamente un archivo de salida
- Sin
print(),message(), ocat()(usarcli_abort(),cli_warn()solo para errores) - Todos los resultados escritos en archivos CSV/RDS/PNG comprometidos
- Dependencias listadas correctamente en Makefile (en orden)
- Los prerrequisitos de directorio de orden-solo usan sintaxis
| - Probado interactivamente, luego con argumentos, luego vía Make
- La regla Makefile usa sintaxis
$^y$@ - Código formateado con
air format R/scriptname.R - Funciones reutilizables extraídas a archivos separados y documentadas con roxygen2
- Ninguna llamada a
install.packages()olibrary()dentro de scripts del pipeline (hacer en Makefile o renv.lock) - No hay valores de ruta o parámetros codificados; todo viene de
shared_inputs.Ro variables de entorno