R + GNU Make 再現可能な研究パターン
これはClaudeスキルです。 Claudeで/skill r-gnumake-orchestratorとロードすると、再現可能なデータ分析パイプラインを構築するためのパターンにアクセスできます。
GNU Makeをファイルターゲットワークフローと標準化されたスクリプトインターフェース契約で使用して、Rの分析パイプラインをオーケストレーションするためのガイドです。
概要
このスキルは、再現可能なデータ分析のための汎用パターンを提供します:
- スクリプトを純粋関数として: 引数を受け取り、入力を読み取り、1つの出力を書き出し、終了する
- ファイルターゲットを持つMakefile: 依存関係が変更されたときのみ再ビルドする
- 共有設定: 全スクリプトで単一の情報源
- Tidyverseエコシステム: 一貫性のあるモダンなRパッケージ
- roxygen2ドキュメント: 再利用可能なすべての関数が文書化されている
- 出力の印刷なし: 結果はコミットされたデータファイルにのみ存在する
これらのパターンは、小規模な探索的分析から、数十のステージと数千の生成アーティファクトを持つ大規模な研究コンペンディウムまでスケーリングします。
RスクリプトとMakefileの規則
Rスクリプトパターン
すべてのパイプラインスクリプトは、.argsを条件付きで定義します。対話的に実行する場合はデフォルト値、Makeから呼び出された場合はcommandArgs()を使用します。
# 引数を定義: 対話的実行時はデフォルト、Makeから呼び出し時はcommandArgs
.args <- if (interactive()) c(
"data/default1.rds",
"data/default2.rds",
"R/shared_inputs.R",
"output/default.csv"
) else commandArgs(trailingOnly = TRUE)
# .argsから位置で抽出
input1 <- .args[1]
input2 <- .args[2]
helper <- .args[3]
output <- .args[4]
source(helper) # COUNTRY, SEED, OUTPUT_DIRなどを読み込む
# ここで: 入力を読み込み、計算し、出力を書き出し、終了
data <- read_rds(input1)
results <- process(data)
write_csv(results, output)
Make内: Rscript $^ $@ はすべての前提条件を順に渡し、最後にターゲットを渡します。
対話的: Rscript R/myscript.R は.argsのデフォルトを使用します(引数は不要)。
Makefileパターン
ファイルターゲットを使用します(疑似ルールは使いません)。前提条件が変更されたときのみ再ビルドします。
# ディレクトリ作成(順序のみ、再ビルドをトリガーしない)
output output/figures output/tables:
mkdir -p $@
# 単一入力 → 単一出力
output/processed.rds: data/raw.csv R/process.R R/shared_inputs.R | output
Rscript $^ $@
# 複数入力 → 単一出力
output/scores.csv: output/model.rds data/test.rds R/score.R R/shared_inputs.R | output
Rscript $^ $@
# 便宜上の疑似ルールのみ(Makeは追跡しない)
.PHONY: all
all: output/scores.csv
重要な構文:
$^= すべての前提条件を順に$@= ターゲットファイル名| output= 順序のみの前提条件(ディレクトリは作成されるが、タイムスタンプは無視される)
共有設定
2つ以上のスクリプトで必要な値はすべて1つのファイルに保持します。スクリプト間で重複はありません。
# 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
# すべてのスクリプトがこれをsourceし、再定義しない
source("R/shared_inputs.R")
ファイル構成
project/
├── Makefile # オーケストレーションルール(ファイルターゲット)
├── R/
│ ├── shared_inputs.R # 設定(全スクリプトでsource)
│ ├── process_data.R # パイプラインスクリプト
│ ├── fit_model.R # (スクリプトあたり1ジョブ)
│ ├── score_model.R
│ ├── helpers.R # 再利用可能な関数(ステージ接頭辞なし)
│ ├── models.R # roxygen2で文書化
│ └── utils.R
├── data/
│ ├── raw/ # ソースデータ
│ ├── processed/ # 生成データ(gitignoreされるかDVCで追跡)
│ └── features/
├── output/
│ ├── figures/ # コミットされる(CIで原稿がレンダリングされるため)
│ ├── tables/ # すべての数値を含む.csvファイル
│ ├── models/ # 適合モデルオブジェクト(.rds)
│ └── predictions/ # モデル出力を含む.csv
└── paper/
├── paper.qmd # 原稿(output/*.csvを読み込む)
└── bibliography.bib
ステージ接頭辞は実行順に並びます:
process_*.R→fit_*.R→score_*.R→visualise_*.R
関数ファイルには接頭辞がありません。 これにより、ドキュメントツールがパイプラインスクリプトと区別できます。
Makeパターンルール
パターンルール(%)を反復的なターゲットに使用します。一度定義すれば、多数に適用できます。
# パターン: 各入力ファイルを処理
INPUTS := file1 file2 file3
# 汎用ルール: 任意の%.csvをoutput/processed_%.rdsに変換
output/processed_%.rds: input/%.csv R/process.R R/shared_inputs.R | output
Rscript $^ $@
# 便宜上のターゲット: すべての処理済みファイル
all_processed: $(patsubst input/%.csv, output/processed_%.rds, $(wildcard input/*.csv))
# 特別なケースの上書き: file1は異なる入力が必要
output/processed_file1.rds: input/file1_v2.csv R/process.R R/shared_inputs.R | output
Rscript $^ $@
ルールあたり1つの出力: 各スクリプトは正確に1つのファイルを書き出すため、各ルールには正確に1つのターゲットがあります。ステージが2つのファイルを必要とする場合は、2つのスクリプトと2つのルールを記述します(「よくあるパターン」の「スクリプトあたり1つの出力」を参照)。
関数の構成
パイプラインスクリプト
スクリプトあたり1ジョブ、スクリプトあたり1出力。各ステージは前のステージが書き出したものを読み込みます。
# R/fit_model.R
# 目的: 準備されたデータにモデルを適合させる
# 入力: data/prepared.rds, R/shared_inputs.R
# 出力: 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)
同じ入力から派生する他のもの(トレーニングデータの要約など)は、独自のスクリプトと独自の出力を持ちます:
# R/summarise_training.R
# 目的: モデルの適合に使用されたデータを要約する
# 入力: data/prepared.rds, R/shared_inputs.R
# 出力: 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
)
関数ファイル
再利用可能な関数は別ファイルに配置します(ステージ接頭辞なし)。roxygen2で文書化します。
# R/models.R
#' データにモデルを適合させる
#'
#' 指定されたパラメータを使用してランダムフォレストを訓練します。
#'
#' @details
#' 入力に関する前提:
#' - dataには列`outcome`(バイナリ: 0/1)があり、すべての予測変数は数値
#' - seedは再現性のための整数
#'
#' @param data outcome列と予測変数列を持つtibble。
#' @param seed 乱数生成のための整数。
#' @param mtry 分割ごとにサンプリングする変数の数の整数。
#'
#' @return 適合モデルオブジェクト(rangerクラス)。
#'
#' @export
fit_model <- function(data, seed = 42, mtry = floor(sqrt(ncol(data) - 1))) {
model <- ranger(outcome ~ ., data = data, seed = seed, mtry = mtry)
model
}
make docsを実行して、roxygenブロックからman/とNAMESPACEを再生成します。
共有設定
研究全体のパラメータの中央ロケーション。すべてのスクリプトがこれをsourceします。
# R/shared_inputs.R
# 研究デザイン
COUNTRY <- "Country_Name"
SEED <- 42
ANALYSIS_YEARS <- 2017:2023
# パス(ハードコードではなく計算)
DATA_DIR <- "data"
OUTPUT_DIR <- "output"
HORIZON <- as.integer(Sys.getenv("HORIZON", "1"))
# モデルのハイパーパラメータ
RF_MTRY <- NULL # sqrt(p)
RF_NTREES <- 500
# ここでは他に何も定義しないでください。
# ステージ固有の値はそのステージのスクリプトに残します。
スタイルガイドライン
Tidyverseスタック
以下のパッケージに統一します:
library(tidyverse) # コア: dplyr, tidyr, ggplot2, readr, purrr
library(fs) # ファイルシステム操作(dir_create, file_exists)
library(cli) # エラー通知(cli_abort, cli_warn)
library(here) # 相対パス(here("R", "script.R"))
library(scales) # プロットフォーマット
library(yardstick) # モデル指標
絶対に使用しないもの:
- 基本Rのサブセット化や
aggregate()(代わりにdplyr::filter(),dplyr::summarise()を使用) map_dfr()(代わりにmap()+list_rbind()を使用)group_by()+ungroup()(代わりに.by =引数を使用)gather(),spread()(代わりにpivot_longer(),pivot_wider()を使用)%>%(代わりにネイティブパイプ|>を使用)function(x)(代わりにラムダ\(x)を使用)- 廃止予定の関数(ヘルプページのライフサイクルバッジを確認)
コードフォーマット
airフォーマッターを使用します(80文字行、2スペースインデントを強制):
air format R/ # ディレクトリ全体をフォーマット
air format R/myscript.R # 1ファイルをフォーマット
設定はリポジトリルートのair.toml(トラッキングされ、個人設定ではない)にあります。
コメント
コードがなぜそれを行うかをコメントし、何を行うかはコメントしないでください(コードを読んで理由が明らかな場合のみ)。コードを単に言い換えるだけのコメントは避けてください。
# 悪い例: コードが明らかに行っていることを繰り返している
# デフォルトのハイパーパラメータでランダムフォレストを適合
model <- ranger(outcome ~ ., data = data)
# 良い例: なぜこの選択をしたのかを説明している
# ここではCARTではなくランダムフォレストを使用。分位点回帰による信頼区間が必要なため
model <- ranger(outcome ~ ., data = data)
# 悪い例: コードから明らか
# yearがANALYSIS_YEARSにある行をフィルタリング
data <- data |> filter(year %in% ANALYSIS_YEARS)
# 良い例: 自明でない理由を説明している
# 2015年はサーベイランスのカバレッジが不完全なため除外(Methods参照)
data <- data |> filter(year >= 2016)
# コメント不要: コードは自己説明的
data <- data |>
select(district, year, outcome) |>
filter(!is.na(outcome))
方法論の選択を説明するときは原稿のMethodsセクションにコメントを記述し、コードコメントは決定が予期しない場合や非標準的な場合にのみ使用します。
出力の印刷禁止
結果はコミットされたCSV、RDS、PNGファイルにのみ存在します。
情報出力のためのmessage(), print(), cat(), sprintf()は使用しないでください。
例外: cli_abort()とcli_warn()(制御フローのみ)。
# 悪い例
message(sprintf("Processed %d rows", nrow(data)))
cat("Model trained\n")
# 良い例(代わりにファイルに書き出す)
meta <- tibble(n_rows = nrow(data))
write_csv(meta, "output/metadata.csv")
原稿はレンダリング時にoutput/metadata.csvを読み取るため、すべての数値がコミットされたファイルに現れます。
検証ワークフロー
新しいスクリプトを作成するときは、Makeルールを追加する前に段階的にテストします。
ステップ1: 対話的にテスト(デフォルト値で)
# 引数なしで動作するか?
Rscript R/myscript.R
# 出力ファイルが作成されたか確認
ls -lh output/
ステップ2: 引数でテスト(Makeを模倣)
# Makeが行うように引数を渡す
Rscript R/myscript.R \
data/raw.csv \
R/shared_inputs.R \
output/result.rds
# ステップ1の出力と比較
ステップ3: Makeルールを追加してテスト
# Makefile内:
output/result.rds: data/raw.csv R/myscript.R R/shared_inputs.R | output
Rscript $^ $@
# テスト
make output/result.rds
# 確認: dataを変更すると再ビルドがトリガーされるか
touch data/raw.csv
make output/result.rds # 再実行されるべき
# 確認: output/result.rdsを変更しても(依存関係ではない)、再ビルドはトリガーされない
touch output/result.rds
make output/result.rds # "up to date"と表示されるべき
ステップ4: ドライラン
make --dry-run output/result.rds # 実行せずに何が実行されるかを表示
例: シンプルな3ステージパイプライン
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)
実行
make # 順にすべてビルド(process → fit → score)
make output/model.rds --always-make # このステップだけ強制再ビルド
make --dry-run # 実行される内容を表示
新規スクリプト用チェックリスト
新しいパイプラインステージを作成するとき:
- スクリプトが引数を順序(入力 → ヘルパー → 出力)で受け取る
- スクリプトに対話的テスト用の
.argsデフォルトが含まれている - スクリプトが
R/shared_inputs.Rをsourceする(研究全体の値を定義しない) - スクリプトが入力を読み込み、計算し、正確に1つの出力ファイルを書き出す
-
print(),message(),cat()がない(エラーにはcli_abort(),cli_warn()のみ使用) - すべての結果がコミットされたCSV/RDS/PNGファイルに書き出される
- 依存関係がMakefileに正しくリストされている(順序通り)
- 順序のみのディレクトリ前提条件は
|構文を使用している - 対話的に、次に引数で、最後にMakeを介してテスト済み
- Makefileルールが
$^と$@構文を使用している - コードが
air format R/scriptname.Rでフォーマットされている - 再利用可能な関数が別ファイルに抽出されている