R + GNU Make 재현 가능한 연구 패턴
이것은 Claude 스킬입니다. Claude에서 /skill r-gnumake-orchestrator로 로드하여 재현 가능한 데이터 분석 파이프라인 구축 패턴에 접근하세요.
R에서 GNU Make를 사용하여 분석 파이프라인을 오케스트레이션하기 위한 가이드입니다. 파일 대상 워크플로우와 표준화된 스크립트 인터페이스 계약을 사용합니다.
개요
이 스킬은 재현 가능한 데이터 분석을 위한 일반 패턴을 제공합니다:
- 스크립트를 순수 함수처럼: 인수 수락, 입력 읽기, 하나의 출력 쓰기, 종료
- 파일 대상 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 패턴
파일 대상 사용 (phony 규칙 금지). 전제조건이 변경될 때만 다시 빌드합니다.
# 디렉토리 생성 (순서 전용, 재빌드 트리거 안 함)
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 $^ $@
# 편의를 위한 Phony 대상만 (Make가 추적하지 않음)
.PHONY: all
all: output/scores.csv
주요 구문:
$^= 모든 전제조건을 순서대로$@= 대상 파일명| output= 순서 전용 전제조건 (디렉토리 생성되지만 타임스탬프는 무시)
공유 설정
두 개 이상의 스크립트가 필요로 하는 모든 값은 하나의 파일에 보관합니다. 스크립트 간 중복 없음.
# 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("R/shared_inputs.R")
파일 구성
project/
├── Makefile # 오케스트레이션 규칙 (파일 대상)
├── R/
│ ├── shared_inputs.R # 설정 (모든 스크립트에서 소싱)
│ ├── process_data.R # 파이프라인 스크립트
│ ├── fit_model.R # (스크립트당 하나의 작업)
│ ├── 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 $^ $@
규칙당 하나의 출력: 모든 스크립트는 정확히 하나의 파일을 쓰므로, 각 규칙은 정확히 하나의 대상을 가집니다. 단계가 두 개의 파일을 필요로 한다면, 두 개의 스크립트와 두 개의 규칙을 작성하세요 (일반 패턴 아래 "출력당 하나의 스크립트" 참조).
함수 구성
파이프라인 스크립트
스크립트당 하나의 작업, 스크립트당 하나의 출력. 각 단계는 이전 단계가 쓴 내용을 읽습니다.
# 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
#' 입력에 대한 가정:
#' - 데이터에 `outcome` 열이 있음 (이진: 0/1) 및 모든 예측변수는 숫자형
#' - seed는 재현성을 위한 정수
#'
#' @param data 결과 및 예측변수 열이 있는 티블.
#' @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를 재생성합니다.
공유 설정
연구 전체 매개변수의 중앙 위치. 모든 스크립트가 소싱합니다.
# 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) # 모델 메트릭
절대 사용하지 말 것:
- 기본 서브셋팅이나
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 # 단일 파일 서식
설정은 air.toml 파일에 저장 (리포지토리 루트에, 개인 설정 아님).
주석
코드가 무엇을 하는지가 아닌 왜 하는지에 주석을 달아야 합니다 (이유가 코드를 읽는 것만으로 명확하지 않은 경우에만). 코드를 단순히 다시 설명하는 주석은 피하십시오.
# 나쁨: 코드가 명백히 하는 것을 반복
# 기본 하이퍼파라미터로 랜덤 포레스트 적합
model <- ranger(outcome ~ ., data = data)
# 좋음: 우리가 왜 이 선택을 했는지 설명
# 분위수 회귀에서 신뢰 구간이 필요하므로 CART 대신 랜덤 포레스트 사용
model <- ranger(outcome ~ ., data = data)
# 나쁨: 코드에서 명백함
# ANALYSIS_YEARS에 있는 연도로 행 필터링
data <- data |> filter(year %in% ANALYSIS_YEARS)
# 좋음: 명확하지 않은 추론 설명
# 불완전한 감시 범위로 인해 2015년 제외 (방법 참조)
data <- data |> filter(year >= 2016)
# 주석 불필요: 코드가 자명함
data <- data |>
select(district, year, outcome) |>
filter(!is.na(outcome))
방법론적 선택을 설명할 때는 원고의 방법 섹션에 주석을 작성하고, 결정이 예상치 못하거나 비표준적인 경우에만 코드 주석을 사용하십시오.
출력 인쇄 금지
결과는 커밋된 CSV, RDS 및 PNG 파일에만 저장됩니다.
정보 출력용 message(), print(), cat() 또는 sprintf() 사용 금지.
예외: cli_abort() 및 cli_warn() (제어 흐름 전용).
# 나쁨
message(sprintf("처리된 행 수: %d", nrow(data)))
cat("모델 훈련 완료\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
# 확인: 데이터 수정 시 재빌드 트리거
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을 소싱 (연구 전체 값을 절대 정의하지 않음) - 스크립트가 입력을 읽고, 계산하고, 정확히 하나의 출력 파일을 씀
-
print(),message()또는cat()없음 (오류에만cli_abort(),cli_warn()사용) - 모든 결과가 커밋된 CSV/RDS/PNG 파일에 기록됨
- 의존성이 Makefile에 올바르게 나열됨 (순서대로)
- 순서 전용 디렉토리 전제조건이
|구문 사용 - 대화형으로, 그 다음 인수로, 그 다음 Make를 통해 테스트됨
- Makefile 규칙이
$^및$@구문 사용 - 코드가
air format R/scriptname.R로 서식 지정됨 - 재사용 가능한 함수가 별도 파일로 추출되고…
… (전체 내용이 번역됨)