Modèles de Recherche Reproductible avec R + GNU Make
Ceci est une compétence Claude. Chargez-la avec /skill r-gnumake-orchestrator dans Claude pour accéder aux modèles de construction de pipelines d'analyse reproductibles.
Un guide pour orchestrer des pipelines d'analyse en R en utilisant GNU Make avec des workflows ciblant les fichiers et un contrat d'interface de script standardisé.
Aperçu
Cette compétence capture des modèles génériques pour l'analyse de données reproductible :
- Les scripts comme des fonctions pures : Acceptent des arguments, lisent les entrées, écrivent une seule sortie, puis se terminent.
- Makefiles avec des cibles de fichiers : Ne reconstruisent que lorsque les dépendances changent.
- Configuration partagée : Une seule source de vérité pour tous les scripts.
- Écosystème Tidyverse : Packages R modernes et cohérents.
- Documentation roxygen2 : Chaque fonction réutilisable est documentée.
- Pas d'affichage de sortie : Les résultats résident uniquement dans des fichiers de données versionnés.
Ces modèles s'adaptent des petites analyses exploratoires aux grands recueils de recherche avec des dizaines d'étapes et des milliers d'artefacts générés.
Conventions pour les Scripts R et les Makefiles
Modèle de Script R
Chaque script de pipeline définit .args conditionnellement : valeurs par défaut pour une utilisation interactive, commandArgs() s'il est appelé depuis Make.
# Définir les arguments : valeurs par défaut pour interactif, commandArgs lorsque appelé depuis Make
.args <- if (interactive()) c(
"data/default1.rds",
"data/default2.rds",
"R/shared_inputs.R",
"output/default.csv"
) else commandArgs(trailingOnly = TRUE)
# Extraire de .args par position
input1 <- .args[1]
input2 <- .args[2]
helper <- .args[3]
output <- .args[4]
source(helper) # charge COUNTRY, SEED, OUTPUT_DIR, etc.
# Maintenant : lire les entrées, calculer, écrire la sortie, puis terminer
data <- read_rds(input1)
results <- process(data)
write_csv(results, output)
Dans Make : Rscript $^ $@ passe toutes les prérequises dans l'ordre, puis la cible.
En interactif : Rscript R/myscript.R utilise les valeurs par défaut de .args (aucun argument nécessaire).
Modèle de Makefile
Utilisez des cibles de fichiers (pas de règles factices). Ne reconstruisez que lorsque les prérequises changent.
# Création de répertoire (ordre seul, ne déclenche pas de reconstructions)
output output/figures output/tables:
mkdir -p $@
# Une entrée → une sortie
output/processed.rds: data/raw.csv R/process.R R/shared_inputs.R | output
Rscript $^ $@
# Entrées multiples → une sortie
output/scores.csv: output/model.rds data/test.rds R/score.R R/shared_inputs.R | output
Rscript $^ $@
# Cibles factices pour commodité uniquement (non suivies par Make)
.PHONY: all
all: output/scores.csv
Syntaxe clé :
$^= toutes les prérequises dans l'ordre$@= nom de fichier de la cible| output= prérequise d'ordre seul (répertoire créé, mais horodatage ignoré)
Configuration Partagée
Un seul fichier contient chaque valeur dont deux scripts ou plus ont besoin. Pas de duplication entre les 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
# Chaque script source ce fichier, ne le redéfinit jamais
source("R/shared_inputs.R")
Organisation des Fichiers
project/
├── Makefile # Règles d'orchestration (cibles de fichiers)
├── R/
│ ├── shared_inputs.R # Configuration (sourcé par tous les scripts)
│ ├── process_data.R # Scripts du pipeline
│ ├── fit_model.R # (une tâche par script)
│ ├── score_model.R
│ ├── helpers.R # Fonctions réutilisables (pas de préfixe d'étape)
│ ├── models.R # Documenté avec roxygen2
│ └── utils.R
├── data/
│ ├── raw/ # Données sources
│ ├── processed/ # Généré (gitignoré ou suivi par DVC)
│ └── features/
├── output/
│ ├── figures/ # Versionné (pour que le manuscrit s'affiche dans CI)
│ ├── tables/ # Fichiers .csv avec tous les nombres
│ ├── models/ # Objets de modèle ajustés (.rds)
│ └── predictions/ # .csv avec les sorties du modèle
└── paper/
├── paper.qmd # Manuscrit (lit output/*.csv)
└── bibliography.bib
Les préfixes d'étape trient par ordre d'exécution :
process_*.R→fit_*.R→score_*.R→visualise_*.R
Les fichiers de fonctions n'ont pas de préfixe, afin que les outils de documentation puissent les distinguer des scripts de pipeline.
Règles de Modèle Make
Utilisez des règles de modèle (%) pour les cibles répétitives. Définissez une fois, appliquez à beaucoup.
# Modèle : traiter chaque fichier d'entrée
INPUTS := file1 file2 file3
# Règle générique : transformer tout %.csv en output/processed_%.rds
output/processed_%.rds: input/%.csv R/process.R R/shared_inputs.R | output
Rscript $^ $@
# Cible de commodité : tous les fichiers traités
all_processed: $(patsubst input/%.csv, output/processed_%.rds, $(wildcard input/*.csv))
# Surcharge pour un cas spécial : file1 nécessite une entrée différente
output/processed_file1.rds: input/file1_v2.csv R/process.R R/shared_inputs.R | output
Rscript $^ $@
Une sortie par règle : chaque script écrit exactement un fichier, donc chaque règle a exactement une cible. Si une étape nécessite deux fichiers, écrivez deux scripts et deux règles (voir « Une sortie par script » dans Modèles Courants).
Organisation des Fonctions
Scripts de Pipeline
Une tâche par script, et une sortie par script. Chaque étape lit ce que l'étape précédente a écrit.
# R/fit_model.R
# Objectif : Ajuster un modèle à des données préparées
# Entrées : data/prepared.rds, R/shared_inputs.R
# Sortie : 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)
Toute autre chose dérivée des mêmes entrées, comme un résumé des données d'entraînement, a son propre script et sa propre sortie :
# R/summarise_training.R
# Objectif : Résumer les données utilisées pour ajuster le modèle
# Entrées : data/prepared.rds, R/shared_inputs.R
# Sortie : 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
)
Fichiers de Fonctions
Les fonctions réutilisables vont dans des fichiers séparés (pas de préfixe d'étape). Documentez avec roxygen2.
# R/models.R
#' Ajuster un modèle à des données
#'
#' Entraîne une forêt aléatoire en utilisant des paramètres spécifiés.
#'
#' @details
#' Hypothèses sur les entrées :
#' - les données ont une colonne `outcome` (binaire : 0/1) et tous les prédicteurs sont numériques
#' - seed est un entier pour la reproductibilité
#'
#' @param data Un tibble avec les colonnes outcome et prédicteurs.
#' @param seed Entier pour la génération de nombres aléatoires.
#' @param mtry Entier ; nombre de variables à échantillonner par division.
#'
#' @return Un objet de modèle ajusté (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
}
Exécutez make docs pour régénérer man/ et NAMESPACE à partir des blocs roxygen.
Configuration Partagée
Emplacement central pour les paramètres de l'étude. Chaque script le source.
# R/shared_inputs.R
# Conception de l'étude
COUNTRY <- "Country_Name"
SEED <- 42
ANALYSIS_YEARS <- 2017:2023
# Chemins (calculés, pas codés en dur)
DATA_DIR <- "data"
OUTPUT_DIR <- "output"
HORIZON <- as.integer(Sys.getenv("HORIZON", "1"))
# Hyperparamètres du modèle
RF_MTRY <- NULL # sqrt(p)
RF_NTREES <- 500
# Ne définissez rien d'autre ici.
# Les valeurs spécifiques à une étape restent dans le script de cette étape.
Règles de Style
Pile Tidyverse
Standardisez sur ces packages :
library(tidyverse) # core : dplyr, tidyr, ggplot2, readr, purrr
library(fs) # opérations sur le système de fichiers (dir_create, file_exists)
library(cli) # signalement d'erreurs (cli_abort, cli_warn)
library(here) # chemins relatifs (here("R", "script.R"))
library(scales) # formatage des graphiques
library(yardstick) # métriques de modèle
N'utilisez jamais :
- Le sous-ensemble de base ou
aggregate()(utilisezdplyr::filter(),dplyr::summarise()) map_dfr()(utilisezmap()+list_rbind())group_by()+ungroup()(utilisez l'argument.by =)gather(),spread()(utilisezpivot_longer(),pivot_wider())%>%(utilisez le pipe natif|>)function(x)(utilisez la lambda\(x))- Les fonctions obsolètes (vérifiez le badge de cycle de vie sur la page d'aide)
Formatage du Code
Utilisez le formateur air (impose des lignes de 80 caractères, indentation de 2 espaces) :
air format R/ # formater tout le répertoire
air format R/myscript.R # formater un fichier
Paramètres dans air.toml à la racine du dépôt (versionné, pas personnel).
Commentaires
Commentez pourquoi le code fait quelque chose, pas quoi (uniquement là où la justification n'est pas évidente en lisant le code). Évitez les commentaires qui ne font que répéter le code.
# Mauvais : répète ce que le code fait évidemment
# Ajuster une forêt aléatoire avec les hyperparamètres par défaut
model <- ranger(outcome ~ ., data = data)
# Bon : explique pourquoi nous avons fait ce choix
# Forêt aléatoire plutôt que CART ici car nous avons besoin d'intervalles de confiance
# à partir de la régression quantile
model <- ranger(outcome ~ ., data = data)
# Mauvais : évident à partir du code
# Filtrer les lignes où l'année est dans ANALYSIS_YEARS
data <- data |> filter(year %in% ANALYSIS_YEARS)
# Bon : explique le raisonnement non évident
# Exclure l'année 2015 en raison d'une couverture de surveillance incomplète (voir Méthodes)
data <- data |> filter(year >= 2016)
# Pas de commentaire nécessaire : le code se suffit à lui-même
data <- data |>
select(district, year, outcome) |>
filter(!is.na(outcome))
Rédigez les commentaires dans la section Méthodes du manuscrit lorsque vous expliquez un choix méthodologique ; utilisez les commentaires de code uniquement lorsque la décision est inattendue ou non standard.
Pas d'Affichage de Sortie
Les résultats résident uniquement dans des fichiers CSV, RDS et PNG versionnés.
Pas de message(), print(), cat(), ou sprintf() pour les sorties informationales.
Exceptions : cli_abort() et cli_warn() (contrôle de flux uniquement).
# Mauvais
message(sprintf("Processed %d rows", nrow(data)))
cat("Model trained\n")
# Bon (écrire dans un fichier à la place)
meta <- tibble(n_rows = nrow(data))
write_csv(meta, "output/metadata.csv")
Le manuscrit lit output/metadata.csv au moment du rendu, donc chaque nombre apparaît dans les fichiers versionnés.
Workflow de Validation
Lorsque vous écrivez un nouveau script, testez progressivement avant d'ajouter des règles Make.
Étape 1 : Tester en Interactif (avec les valeurs par défaut)
# Est-ce que cela fonctionne sans arguments ?
Rscript R/myscript.R
# Vérifier que les fichiers de sortie ont été créés
ls -lh output/
Étape 2 : Tester avec des Arguments (imite Make)
# Passer les arguments comme Make le ferait
Rscript R/myscript.R \
data/raw.csv \
R/shared_inputs.R \
output/result.rds
# Comparer avec la sortie de l'étape 1
Étape 3 : Ajouter une Règle Make et Tester
# Dans le Makefile :
output/result.rds: data/raw.csv R/myscript.R R/shared_inputs.R | output
Rscript $^ $@
# Tester
make output/result.rds
# Vérifier : modifier les données devrait déclencher une reconstruction
touch data/raw.csv
make output/result.rds # Devrait réexécuter
# Vérifier : modifier output/result.rds (pas une dépendance) ne déclenche pas de reconstruction
touch output/result.rds
make output/result.rds # Devrait dire "up to date"
Étape 4 : Simulation
make --dry-run output/result.rds # Affiche ce qui serait exécuté sans exécuter
Exemple : Pipeline Simple en 3 Étapes
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)
Exécuter
make # Construit tout dans l'ordre (process → fit → score)
make output/model.rds --always-make # Forcer la reconstruction de cette étape uniquement
make --dry-run # Afficher ce qui serait exécuté
Liste de Vérification pour les Nouveaux Scripts
Lors de l'écriture d'une nouvelle étape de pipeline :
- Le script accepte des arguments dans l'ordre : entrées → helper → sortie
- Le script inclut les valeurs par défaut de
.argspour les tests interactifs - Le script source
R/shared_inputs.R(ne définit jamais de valeurs à l'échelle de l'étude) - Le script lit ses entrées, calcule et écrit exactement un fichier de sortie
- Pas de
print(),message()oucat()(utilisezcli_abort(),cli_warn()pour les erreurs uniquement) - Tous les résultats écrits dans des fichiers CSV/RDS/PNG versionnés
- Les dépendances listées correctement dans le Makefile (dans l'ordre)
- Les prérequises de répertoire d'ordre seul utilisent la syntaxe
| - Testé en interactif, puis avec arguments, puis via Make
- La règle du Makefile utilise la syntaxe
$^et$@ - Code formaté avec
air format R/scriptname.R - Les fonctions réutilisables extraites dans des fichiers séparés
…
[truncated for translation]