CommunityRecherche & Datenanalysegithub.com

jamesmbaazam/r-gnumake-orchestrator

Generische Muster zur Orchestrierung von Datenanalyse-Pipelines in R mit GNU Make, dateibasierten Workflows und standardisierten Skriptverträgen

Was ist r-gnumake-orchestrator?

Dieses Skill bietet ein strukturiertes Framework für reproduzierbare Datenanalyse-Pipelines in R unter Verwendung von GNU Make. Es etabliert einen standardisierten Vertrag für R-Skripte, die als reine Funktionen agieren: Sie akzeptieren Argumente, lesen Eingaben, schreiben genau eine Ausgabe und beenden sich. Makefiles nutzen dateibasierte Ziele, sodass nur dann neu gebaut wird, wenn sich Abhängigkeiten ändern – das spart Zeit in großen Analysen. Das Skill unterstützt Claude Code, Cursor und Codex sowie jede andere KI, die kontextreiche Anweisungen verarbeiten kann. Es behandelt Skriptkonventionen (`.args`-Muster), Makefile-Regeln (mit `$^` und `$@`), geteilte Konfiguration (`R/shared_inputs.R`), Dateiorganisation mit Stage-Präfixen und Validierungsworkflows. Die Stärke liegt in der Kombination von Skriptverträgen, Make's Datei-Tracking und einer disziplinierten Projektstruktur, die von kleinen Analysen bis zu großen Forschungskompendien mit Dutzenden von Stufen skaliert.

Funktioniert mit✓Claude Code~Codex CLI~Cursor
npx skills add https://github.com/jamesmbaazam/r-gnumake-orchestrator/tree/HEAD

Installed? Explore more Recherche & Datenanalyse skills: affaan-m/uspto-database, affaan-m/scholar-evaluation, affaan-m/literature-review · View all 6 →

In Ihrer bevorzugten KI fragen

Öffnet einen neuen Chat, in dem dieser Agent-Skill bereits geladen ist.

Dokumentation

R + GNU Make Reproduzierbare Forschungspatterns

Dies ist ein Claude-Skill. Lade es mit /skill r-gnumake-orchestrator in Claude, um Zugriff auf Patterns zum Erstellen reproduzierbarer Datenanalyse-Pipelines zu erhalten.

Ein Leitfaden zur Orchestrierung von Analyse-Pipelines in R mit GNU Make, dateibasierten Workflows und einem standardisierten Skriptschnittstellenvertrag.

Übersicht

Dieses Skill erfasst generische Patterns für reproduzierbare Datenanalyse:

  • Skripte als reine Funktionen: Akzeptieren Argumente, lesen Eingaben, schreiben eine Ausgabe, beenden sich
  • Makefiles mit Dateizielen: Nur neu bauen, wenn sich Abhängigkeiten ändern
  • Geteilte Konfiguration: Eine einzige Quelle der Wahrheit für alle Skripte
  • Tidyverse-Ökosystem: Konsistente, moderne R-Pakete
  • roxygen2-Dokumentation: Jede wiederverwendbare Funktion wird dokumentiert
  • Keine Ausgabeausgabe: Ergebnisse leben nur in kommittierten Datendateien

Diese Patterns skalieren von kleinen explorativen Analysen bis zu großen Forschungskompendien mit Dutzenden von Stufen und Tausenden generierten Artefakten.

R-Skripte und Makefile-Konventionen

R-Skript-Pattern

Jedes Pipeline-Skript definiert .args bedingt: interaktive Standardwerte, wenn es in der Konsole läuft, commandArgs() wenn es von Make aufgerufen wird.

# Definiere Argumente: Standardwerte für interaktiven Modus, commandArgs wenn von Make aufgerufen
.args <- if (interactive()) c(
  "data/default1.rds",
  "data/default2.rds",
  "R/shared_inputs.R",
  "output/default.csv"
) else commandArgs(trailingOnly = TRUE)

# Extrahiere aus .args nach Position
input1 <- .args[1]
input2 <- .args[2]
helper <- .args[3]
output <- .args[4]

source(helper)  # lädt COUNTRY, SEED, OUTPUT_DIR, etc.

# Jetzt: Eingaben lesen, berechnen, Ausgabe schreiben, beenden
data <- read_rds(input1)
results <- process(data)
write_csv(results, output)

In Make: Rscript $^ $@ übergibt alle Voraussetzungen der Reihe nach, dann das Ziel.
Interaktiv: Rscript R/myscript.R verwendet .args-Standardwerte (keine Argumente nötig).

Makefile-Pattern

Verwende Dateiziele (keine Phony-Regeln). Nur neu bauen, wenn sich Voraussetzungen ändern.

# Verzeichniserstellung (order-only, löst keine Neubauten aus)
output output/figures output/tables:
	mkdir -p $@

# Eine Eingabe → eine Ausgabe
output/processed.rds: data/raw.csv R/process.R R/shared_inputs.R | output
	Rscript $^ $@

# Mehrere Eingaben → eine Ausgabe
output/scores.csv: output/model.rds data/test.rds R/score.R R/shared_inputs.R | output
	Rscript $^ $@

# Phony-Ziele nur zur Bequemlichkeit (nicht von Make verfolgt)
.PHONY: all
all: output/scores.csv

Wichtige Syntax:

  • $^ = alle Voraussetzungen in Reihenfolge
  • $@ = Zieldateiname
  • | output = order-only-Voraussetzung (Verzeichnis erstellt, aber Zeitstempel ignoriert)

Geteilte Konfiguration

Eine Datei enthält jeden Wert, den zwei oder mehr Skripte benötigen. Keine Duplikation über Skripte hinweg.

# 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

# Jedes Skript sourced dies, definiert es nie neu
source("R/shared_inputs.R")

Dateiorganisation

project/
├── Makefile                 # Orchestrierungsregeln (Dateiziele)
├── R/
│   ├── shared_inputs.R      # Konfiguration (von allen Skripten gesourced)
│   ├── process_data.R       # Pipeline-Skripte
│   ├── fit_model.R          # (ein Job pro Skript)
│   ├── score_model.R
│   ├── helpers.R            # Wiederverwendbare Funktionen (kein Stage-Präfix)
│   ├── models.R             # roxygen2-dokumentiert
│   └── utils.R
├── data/
│   ├── raw/                 # Quelldaten
│   ├── processed/           # Generiert (gitignoriert oder DVC-verfolgt)
│   └── features/
├── output/
│   ├── figures/             # Kommittiert (damit Manuskript in CI rendert)
│   ├── tables/              # .csv-Dateien mit allen Zahlen
│   ├── models/              # Angepasste Modellobjekte (.rds)
│   └── predictions/         # .csv mit Modellausgaben
└── paper/
    ├── paper.qmd            # Manuskript (liest output/*.csv)
    └── bibliography.bib

Stage-Präfixe sortieren in Ausführungsreihenfolge:

  • process_*.R → fit_*.R → score_*.R → visualise_*.R

Funktionsdateien haben kein Präfix, damit Dokumentationstools sie von Pipeline-Skripten unterscheiden können.

Make-Pattern-Regeln

Verwende Pattern-Regeln (%) für wiederholte Ziele. Einmal definieren, auf viele anwenden.

# Pattern: Verarbeite jede Eingabedatei
INPUTS := file1 file2 file3

# Generische Regel: Transformiere jede %.csv in output/processed_%.rds
output/processed_%.rds: input/%.csv R/process.R R/shared_inputs.R | output
	Rscript $^ $@

# Bequemlichkeitsziel: Alle verarbeiteten Dateien
all_processed: $(patsubst input/%.csv, output/processed_%.rds, $(wildcard input/*.csv))

# Überschreibung für Spezialfall: file1 benötigt andere Eingabe
output/processed_file1.rds: input/file1_v2.csv R/process.R R/shared_inputs.R | output
	Rscript $^ $@

Eine Ausgabe pro Regel: Jedes Skript schreibt genau eine Datei, also hat jede Regel genau ein Ziel. Wenn eine Stufe zwei Dateien benötigt, schreibe zwei Skripte und zwei Regeln (siehe "Eine Ausgabe pro Skript" unter Allgemeine Patterns).

Funktionsorganisation

Pipeline-Skripte

Ein Job pro Skript und eine Ausgabe pro Skript. Jede Stufe liest, was die vorherige Stufe geschrieben hat.

# R/fit_model.R
# Zweck: Ein Modell an vorbereitete Daten anpassen
# Eingaben: data/prepared.rds, R/shared_inputs.R
# Ausgabe: 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)

Alles andere, das aus denselben Eingaben abgeleitet wird, wie eine Zusammenfassung der Trainingsdaten, bekommt ein eigenes Skript und seine eigene Ausgabe:

# R/summarise_training.R
# Zweck: Die Daten zusammenfassen, die zum Anpassen des Modells verwendet wurden
# Eingaben: data/prepared.rds, R/shared_inputs.R
# Ausgabe: 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
)

Funktionsdateien

Wiederverwendbare Funktionen kommen in separate Dateien (kein Stage-Präfix). Dokumentiere mit roxygen2.

# R/models.R

#' Passen Sie ein Modell an Daten an
#'
#' Trainiert einen Random Forest mit angegebenen Parametern.
#'
#' @details
#' Annahmen über Eingaben:
#' - Daten haben Spalte `outcome` (binär: 0/1) und alle Prädiktoren sind numerisch
#' - seed ist eine ganze Zahl für Reproduzierbarkeit
#'
#' @param data Ein Tibble mit outcome- und Prädiktorspalten.
#' @param seed Ganze Zahl für die Zufallszahlengenerierung.
#' @param mtry Ganze Zahl; Anzahl der Variablen, die pro Split abgetastet werden.
#'
#' @return Ein angepasstes Modellobjekt (ranger-Klasse).
#'
#' @export
fit_model <- function(data, seed = 42, mtry = floor(sqrt(ncol(data) - 1))) {
  model <- ranger(outcome ~ ., data = data, seed = seed, mtry = mtry)
  model
}

Führe make docs aus, um man/ und NAMESPACE aus roxygen-Blöcken neu zu generieren.

Geteilte Konfiguration

Zentraler Ort für studienweite Parameter. Jedes Skript sourced dies.

# R/shared_inputs.R

# Studiendesign
COUNTRY <- "Country_Name"
SEED <- 42
ANALYSIS_YEARS <- 2017:2023

# Pfade (berechnet, nicht hartcodiert)
DATA_DIR <- "data"
OUTPUT_DIR <- "output"
HORIZON <- as.integer(Sys.getenv("HORIZON", "1"))

# Modell-Hyperparameter
RF_MTRY <- NULL  # sqrt(p)
RF_NTREES <- 500

# Definieren Sie hier nichts anderes.
# Stufenspezifische Werte bleiben im Skript ihrer Stufe.

Stilrichtlinien

Tidyverse-Stack

Standardisiere auf diese Pakete:

library(tidyverse)   # Kern: dplyr, tidyr, ggplot2, readr, purrr
library(fs)          # Dateisystemoperationen (dir_create, file_exists)
library(cli)         # Fehlermeldungen (cli_abort, cli_warn)
library(here)        # Relative Pfade (here("R", "script.R"))
library(scales)      # Plot-Formatierung
library(yardstick)   # Modellmetriken

Nie verwenden:

  • Base-Subsetting oder aggregate() (verwende dplyr::filter(), dplyr::summarise())
  • map_dfr() (verwende map() + list_rbind())
  • group_by() + ungroup() (verwende .by =-Argument)
  • gather(), spread() (verwende pivot_longer(), pivot_wider())
  • %>% (verwende nativen Pipe-Operator |>)
  • function(x) (verwende Lambda \(x))
  • Überholte Funktionen (überprüfe Lifecycle-Badge auf Hilfeseite)

Code-Formatierung

Verwende air-Formatter (erzwingt 80-Zeichen-Zeilen, 2-Leerzeichen-Einrückung):

air format R/                  # formatiere gesamtes Verzeichnis
air format R/myscript.R        # formatiere eine Datei

Einstellungen in air.toml im Repo-Root (verfolgt, nicht persönlich).

Kommentare

Kommentiere warum der Code etwas tut, nicht was (nur wo die Begründung nicht offensichtlich aus dem Code hervorgeht). Vermeide Kommentare, die nur den Code wiederholen.

# Schlecht: wiederholt, was der Code offensichtlich tut
# Passe einen Random Forest mit Standard-Hyperparametern an
model <- ranger(outcome ~ ., data = data)

# Gut: erklärt, warum wir diese Wahl getroffen haben
# Random Forest statt CART hier, weil wir Konfidenzintervalle aus Quantilsregression brauchen
model <- ranger(outcome ~ ., data = data)

# Schlecht: offensichtlich aus dem Code
# Filtere Zeilen, bei denen Jahr in ANALYSIS_YEARS ist
data <- data |> filter(year %in% ANALYSIS_YEARS)

# Gut: erklärt die nicht offensichtliche Begründung
# Schließe Jahr 2015 aus wegen unvollständiger Überwachungsabdeckung (siehe Methoden)
data <- data |> filter(year >= 2016)

# Kein Kommentar nötig: Der Code erklärt sich selbst
data <- data |>
  select(district, year, outcome) |>
  filter(!is.na(outcome))

Schreibe Kommentare in den Methodenabschnitt des Manuskripts, wenn eine methodische Wahl erklärt wird; verwende Code-Kommentare nur, wenn die Entscheidung unerwartet oder nicht standardmäßig ist.

Keine Ausgabeausgabe

Ergebnisse leben nur in kommittierten CSV-, RDS- und PNG-Dateien.
Kein message(), print(), cat() oder sprintf() für informative Ausgaben.
Ausnahmen: cli_abort() und cli_warn() (nur zur Steuerung des Ablaufs).

# Schlecht
message(sprintf("%d Zeilen verarbeitet", nrow(data)))
cat("Modell trainiert\n")

# Gut (stattdessen in Datei schreiben)
meta <- tibble(n_rows = nrow(data))
write_csv(meta, "output/metadata.csv")

Das Manuskript liest output/metadata.csv zur Rendering-Zeit, also erscheint jede Zahl in kommittierten Dateien.

Validierungsworkflow

Wenn du ein neues Skript schreibst, teste progressiv, bevor du Make-Regeln hinzufügst.

Schritt 1: Interaktiv testen (mit Standardwerten)

# Funktioniert es ohne Argumente?
Rscript R/myscript.R
# Überprüfe, ob Ausgabedateien erstellt wurden
ls -lh output/

Schritt 2: Mit Argumenten testen (imitiert Make)

# Übergib Argumente, wie Make es tun würde
Rscript R/myscript.R \
  data/raw.csv \
  R/shared_inputs.R \
  output/result.rds
# Vergleiche mit Ausgabe von Schritt 1

Schritt 3: Make-Regel hinzufügen und testen

# In Makefile:
output/result.rds: data/raw.csv R/myscript.R R/shared_inputs.R | output
	Rscript $^ $@

# Teste es
make output/result.rds

# Überprüfe: Ändern von Daten sollte Neubau auslösen
touch data/raw.csv
make output/result.rds  # Sollte neu laufen

# Überprüfe: Ändern von output/result.rds (keine Abhängigkeit) löst keinen Neubau aus
touch output/result.rds
make output/result.rds  # Sollte "up to date" sagen

Schritt 4: Trockenlauf

make --dry-run output/result.rds  # Zeige, was laufen würde, ohne es auszuführen

Beispiel: Einfache 3-Stufen-Pipeline

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)

Ausführen

make            # Baut alles der Reihe nach (process → fit → score)
make output/model.rds --always-make  # Erzwinge Neubau nur dieses Schritts
make --dry-run  # Zeige, was laufen würde

Checkliste für neue Skripte

Beim Schreiben einer neuen Pipeline-Stufe:

  • Skript akzeptiert Argumente in Reihenfolge: Eingaben → helper → Ausgabe
  • Skript enthält .args-Standardwerte für interaktives Testen
  • Skript sourced R/shared_inputs.R (definiert nie studienweite Werte)
  • Skript liest seine Eingaben, berechnet und schreibt genau eine Ausgabedatei
  • Kein print(), message() oder cat() (verwende cli_abort(), cli_warn() nur für Fehler)
  • Alle Ergebnisse in kommittierte CSV-/RDS-/PNG-Dateien geschrieben
  • Abhängigkeiten im Makefile korrekt aufgelistet (in Reihenfolge)
  • Order-only-Verzeichnisvoraussetzungen verwenden |-Syntax
  • Interaktiv getestet, dann mit Argumenten, dann via Make
  • Makefile-Regel verwendet $^- und $@-Syntax
  • Code mit air format R/scriptname.R formatiert
  • Wiederverwendbare Funktionen in separate Dateien ausgelagert und mit roxygen2 dokumentiert

Verwandte Skills