
Batch Cohort
- 45 installs
- 236 repo stars
- Updated August 3, 2026
- aperivue/medsci-skills
Batch Cohort is a skill that generates many analysis scripts from one validated methodology template by swapping exposure/outcome variable combinations.
About
Batch Cohort generates many analysis scripts from a single validated methodology template, each differing only in the exposure/outcome variable combination. A researcher uses it to systematically explore multiple exposure-to-outcome pairs on one database (KNHANES, NHANES, NHIS, or any cleaned cohort) and collect results into a summary matrix. It identifies template slot variables, adjusts covariates per combination, and generates a master runner script.
- Generates N analysis scripts from one validated template by swapping exposure/outcome variables
- Implements the '80-person team' pattern: one method, many variable combinations
- Produces batch R/Python code plus a summary result matrix
Batch Cohort by the numbers
- 45 all-time installs (skills.sh)
- Ranked #964 of 2,064 Data Science & ML skills by installs in the Skillselion catalog
- Data as of Aug 5, 2026 (Skillselion catalog sync)
batch-cohort capabilities & compatibility
- Capabilities
- batch analysis · cohort analysis · code generation
- Use cases
- data analysis · research
What batch-cohort says it does
Generate N analysis scripts from a single methodology template × multiple exposure/outcome combinations.
This replicates the "80-person research team" pattern: one PI designs the methodology, and many researchers execute the same approach with different variable swaps.
npx skills add https://github.com/aperivue/medsci-skills --skill batch-cohortAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 45 |
|---|---|
| repo stars | ★ 236 |
| Last updated | August 3, 2026 |
| Repository | aperivue/medsci-skills ↗ |
What it does
Generate N cohort analysis scripts from one validated template by swapping exposure/outcome variable combinations and collect the results.
Who is it for?
Mass-producing cohort analyses that reuse one validated method across many variable pairs
When should I use this skill?
when a researcher wants to run one validated method across many exposure/outcome combinations on the same database
What you get
A batch of variable-swapped analysis scripts, a runner, and a summary result matrix
- batch R/Python analysis scripts
- master runner script
- summary result matrix
By the numbers
- generates N scripts from 1 template
- supports 2-country cross-national paired mode
Files
Batch Cohort Analysis Skill
You are assisting a medical researcher in generating multiple analysis scripts from a single validated methodology template, each differing only in the exposure/outcome variable combination. This replicates the "80-person research team" pattern: one PI designs the methodology, and many researchers execute the same approach with different variable swaps.
When to Use
- Researcher has a validated analysis template (e.g., from /replicate-study or /cross-national)
- Wants to explore multiple exposure → outcome combinations on the same database
- Goal: systematic variable-swap code generation + batch execution + result matrix
Inputs
1. Database path(s): CSV/SAS data files (KNHANES, NHANES, NHIS, or any cleaned cohort) 2. Methodology template: One of:
- Path to a validated R/Python analysis script (from /replicate-study or /cross-national)
- A paper type template name:
nhis_cohort,cross_national,survey_weighted - A source paper to extract methodology from (falls back to /replicate-study Phase 1)
3. Combination spec: A list of exposure/outcome pairs, provided as:
- Inline list:
exposures: [depression, obesity, smoking]; outcomes: [diabetes, hypertension, CVD] - CSV file with columns:
exposure,outcome, (optional)subgroup_vars "all"keyword: generates all pairwise combinations from the lists
Optional Inputs
- Covariate set: Fixed covariate list for all analyses (default: use template's set)
- Subgroup variables: Variables to stratify by (default: sex, age group)
- Output format:
code_only(just scripts) |execute(run + collect results) |full(code + results + summary) - Cross-national mode: If TRUE, generates paired scripts for both countries per combination
Workflow
Phase 1: Template Validation
1. Read the methodology template (R script or paper type reference). 2. Identify the slot variables — parts that change per combination:
EXPOSURE_VAR: raw variable name in the databaseEXPOSURE_LABEL: human-readable label for tables/figuresEXPOSURE_CODING: how to derive binary/categorical exposureOUTCOME_VAR: raw variable nameOUTCOME_LABEL: human-readable labelOUTCOME_CODING: how to derive binary outcome
3. Verify the template runs successfully on at least one combination before batch generation. 4. Output: template summary with identified slots → user approval.
Phase 2: Variable Specification
For each exposure and outcome in the combination spec:
1. Look up the variable in the database:
- KNHANES: check variable name exists in the CSV header
- NHANES: check which table contains the variable (use codebook.csv if available)
- NHIS: check claims code or variable name
2. Define coding:
- Binary: threshold or category mapping (e.g.,
HE_glu >= 126 → diabetes = 1) - Categorical: level definitions (e.g.,
smoking: current/former/never)
3. Check covariate overlap: If the exposure IS one of the standard covariates, remove it from the adjustment set for that analysis (no self-adjustment). 4. Output: combination matrix with all variable specifications.
| # | Exposure | Exposure Coding | Outcome | Outcome Coding | Covariates (adjusted) | Notes |
|---|----------|-----------------|---------|----------------|----------------------|-------|
| 1 | Depression (PHQ≥10) | BP_PHQ sum ≥10 | Diabetes | HE_glu≥126|HbA1c≥6.5|DE1_dg=1 | age,sex,edu,income,smoking,alcohol,obesity,CVD | — |
| 2 | Obesity (BMI≥25) | HE_obe ≥4 | Diabetes | same | age,sex,edu,income,smoking,alcohol,depression,CVD | obesity removed from covariates |
| ... | | | | | | |Phase 3: Batch Code Generation
For each combination in the matrix:
1. Clone the template script. 2. Replace slot variables with the combination-specific values. 3. Adjust covariates: Remove exposure variable from covariate list if present. 4. Set output paths: Each combination gets its own results subdirectory. 5. Generate a master runner script (run_all.R or run_all.sh) that:
- Executes all N scripts sequentially (or in parallel via
future/parallel) - Captures errors per script without stopping the batch
- Logs execution time per analysis
Phase 4: Batch Execution (if execute or full mode)
1. Run the master script. 2. Collect results from each combination's output directory. 3. Handle failures gracefully:
- Log which combinations failed and why
- Common failures: convergence issues, too few events, empty subgroups
- Suggest fixes for failed combinations
Phase 5: Summary Matrix
Aggregate all results into a single summary:
Main Results Matrix (summary_matrix.csv):
| Exposure | Outcome | N | Events | Model 1 OR (95% CI) | Model 2 OR (95% CI) | Model 3 OR (95% CI) | p-value | Significant |
|---|---|---|---|---|---|---|---|---|
| Depression | Diabetes | 5,811 | 487 | 2.14 (1.52–3.01) | 1.89 (1.33–2.69) | 1.36 (0.91–2.05) | 0.137 | No |
| Obesity | Diabetes | 5,811 | 487 | 3.45 (2.71–4.39) | 3.38 (2.65–4.32) | 3.12 (2.42–4.02) | <0.001 | Yes |
| ... |
Subgroup Summary (subgroup_matrix.csv): Same format, stratified by subgroup variables.
Heatmap (optional): Visual matrix of effect sizes × significance, exposure on Y-axis, outcome on X-axis.
Output Files
{working_dir}/batch_{timestamp}/
├── README.md — Batch run summary (N combinations, template used, date)
├── combination_matrix.csv — All exposure/outcome specs with coding
├── template/
│ └── base_template.R — The validated template (frozen copy)
├── scripts/
│ ├── 01_depression_diabetes.R
│ ├── 02_obesity_diabetes.R
│ ├── ...
│ └── run_all.R — Master execution script
├── results/
│ ├── 01_depression_diabetes/
│ │ ├── table1.csv
│ │ ├── main_results.csv
│ │ └── subgroup_results.csv
│ ├── 02_obesity_diabetes/
│ │ └── ...
│ └── ...
├── summary/
│ ├── summary_matrix.csv — Main results across all combinations
│ ├── subgroup_matrix.csv — Subgroup results across all combinations
│ ├── failed_runs.csv — Combinations that failed + error messages
│ └── heatmap.png — Optional effect size × significance visual
└── logs/
└── batch_execution.log — Timing + error logCritical Rules
1. Never modify the core methodology across combinations — only swap exposure/outcome/covariates. 2. Remove self-adjustment: If exposure = BMI, remove obesity from covariates. If exposure = education/income, remove the same variable from covariates. If outcome = MetS, consider removing obesity from covariates. Document all removals. 3. Weighted analysis mandatory for KNHANES/NHANES/NHIS — inherited from template. 4. Event count check: Before running, verify each outcome has ≥10 events per covariate (EPV rule). Flag underpowered combinations. 5. Multiple comparisons: When generating >5 combinations, include a Bonferroni-corrected significance column in the summary matrix. Add a note about exploratory vs confirmatory framing. 6. Reproducibility: Freeze the template version. Include a SHA256 hash of the data file in README. 7. No p-hacking framing: The summary matrix is for hypothesis generation, not confirmation. State this explicitly in README and any manuscript output. 8. Outcome definitions MUST include physician diagnosis: Diabetes = FPG≥126 OR HbA1c≥6.5 OR physician-diagnosed (KNHANES: DE1_dg=1, NHANES: DIQ010="Yes"). Hypertension = SBP≥140 OR DBP≥90 OR physician-diagnosed (KNHANES: DI1_dg=1, NHANES: BPQ020="Yes"). Lab-only definitions systematically overestimate exposure→outcome associations (validated: Joo 2026 replication showed US depression→DM wOR 1.92 without vs 1.54 with physician dx). 9. Full covariate set is default: Always use 8 covariates (age, sex, education, income, smoking, alcohol, obesity, CVD) unless explicitly justified. Minimal models (age+sex+BMI only) overestimate effects due to residual confounding. 10. Generated-code quality gate: Because this skill emits N near-identical scripts, a single reproducibility slip (a missing seed, an absolute path, a hand-typed data literal) replicates across the whole batch. After Phase 3, lint the generated scripts with the /analyze-stats code-quality gate (check_generated_code.py --code-dir {batch_dir} --strict) and clear every Major (MISSING_SEED, HARDCODED_DATA_LITERAL, HARDCODED_ABS_PATH, INPLACE_SOURCE_OVERWRITE) before batch execution.
Cross-National Batch Mode
When cross_national: true:
- Generate paired scripts for each combination (Korea + US)
- Summary matrix includes both countries side-by-side
- Direction agreement column: ✓ if both countries show same direction of effect
- Uses /cross-national skill's dual-survey-design approach
Integration with Upstream Skills
| Need | Skill |
|---|---|
| Variable coding lookup | analyze-stats survey_weighted guide |
| Template creation from paper | /replicate-study Phase 1–3 |
| Cross-national paired analysis | /cross-national |
| ICD-10 claims algorithms | analyze-stats nhis_icd10_mapping guide |
| Write manuscript from results | /write-paper (nhis_cohort or cross_national type) |
| Figure generation | /make-figures (forest plot of all combinations) |
Example Invocations
Basic: Single DB, Multiple Exposures × Single Outcome
/batch-cohort
DB: /path/to/knhanes/HN18.csv
Template: /path/to/validated_analysis.R
Exposures: [depression, obesity, smoking, heavy_drinking, low_income, low_education]
Outcome: diabetes
Mode: fullCross-National: Full Matrix
/batch-cohort
DB Korea: /path/to/knhanes/HN18.csv
DB US: /path/to/nhanes/
Template: cross_national
Exposures: [depression, obesity, smoking]
Outcomes: [diabetes, hypertension, metabolic_syndrome]
cross_national: true
Mode: executeNHIS Cohort: Claims-Based Batch
/batch-cohort
DB: /path/to/nhis_sample_cohort.csv
Template: nhis_cohort
Exposures: [atrial_fibrillation, heart_failure, COPD, CKD]
Outcomes: [all_cause_mortality, cardiovascular_death, stroke]
Mode: code_onlyAnti-Hallucination
- Never fabricate variable names, dataset column names, or variable codings. If a variable mapping is uncertain, output
[VERIFY: variable_name]and ask the user to confirm against the data dictionary. - Never fabricate statistical results — no invented p-values, effect sizes, confidence intervals, or sample sizes. All numbers must come from executed code output.
- Never generate references from memory. Use
/search-litfor all citations. - If a function, package, or API does not exist or you are unsure, say so explicitly rather than guessing.
#!/usr/bin/env Rscript
# ==============================================================================
# KNHANES Survey-Weighted Logistic Regression — Batch Template
# Combo: {{COMBO_ID}} | {{EXPOSURE_LABEL}} → {{OUTCOME_LABEL}}
#
# Slot variables (replaced by batch_template_generator.R):
# {{EXPOSURE_VAR}}, {{EXPOSURE_LABEL}}, {{EXPOSURE_CODING}}
# {{OUTCOME_VAR}}, {{OUTCOME_LABEL}}, {{OUTCOME_CODING}}
# {{RESULTS_DIR}}, {{COMBO_ID}}, {{COVARIATES_REMOVE}}
#
# Data: KNHANES single-cycle CSV (e.g., HN18.csv)
# ==============================================================================
suppressPackageStartupMessages({
library(survey)
library(dplyr)
library(tidyr)
})
# --- CONFIG (edit these paths for your setup) ---
DATA_PATH <- "{{DATA_PATH}}" # e.g., /path/to/HN18.csv
RESULTS_DIR <- "{{RESULTS_DIR}}"
COMBO_ID <- "{{COMBO_ID}}"
AGE_MIN <- 20
AGE_MAX <- Inf
# --- Standard Covariates ---
# Full set; exposure-specific removals applied below
ALL_COVARIATES <- c("age", "sex_binary", "edu_binary", "income_binary",
"smoking_3cat", "alcohol_3cat", "obesity_binary", "cvd_binary")
REMOVE_COVARS <- c({{COVARIATES_REMOVE}})
COVARIATES <- setdiff(ALL_COVARIATES, REMOVE_COVARS)
# --- Load & Clean ---
raw <- read.csv(DATA_PATH)
df <- raw %>%
filter(age >= AGE_MIN, age <= AGE_MAX) %>%
mutate(
# --- Demographics ---
sex_binary = ifelse(sex == 1, 0, 1), # 0=male, 1=female
edu_binary = ifelse(edu %in% 1:3, 0, 1), # 0=non-college, 1=college
income_binary = ifelse(incm %in% 1:3, 0, 1), # 0=bottom80, 1=top20
# --- Standard Covariates ---
smoking_3cat = case_when(
BS3_1 %in% c(1, 2) ~ "current",
BS3_1 == 3 ~ "former",
BS3_1 == 8 ~ "never",
TRUE ~ NA_character_
),
alcohol_3cat = case_when(
BD1_11 %in% 2:6 ~ "frequent",
BD1_11 == 1 ~ "occasional",
BD1_11 == 8 ~ "never",
TRUE ~ NA_character_
),
obesity_binary = ifelse(HE_obe >= 4, 1, 0),
cvd_binary = ifelse(DI4_dg == 1 | DI5_dg == 1 | DI6_dg == 1, 1, 0),
# --- Exposure (SLOT) ---
exposure = {{EXPOSURE_CODING}},
# --- Outcome (SLOT) ---
outcome = {{OUTCOME_CODING}}
) %>%
filter(!is.na(exposure), !is.na(outcome))
cat(sprintf("[%s] N after filtering: %d (exposure=1: %d, outcome=1: %d)\n",
COMBO_ID, nrow(df), sum(df$exposure == 1), sum(df$outcome == 1)))
# --- EPV Check ---
n_events <- min(sum(df$outcome == 1), sum(df$outcome == 0))
n_params <- length(COVARIATES) + 1 # +1 for exposure
epv <- n_events / n_params
if (epv < 10) {
warning(sprintf("[%s] EPV = %.1f (< 10). Results may be unreliable.", COMBO_ID, epv))
}
# --- Survey Design ---
design <- svydesign(
id = ~psu,
strata = ~kstrata,
weights = ~wt_itvex,
nest = TRUE,
data = df
)
# --- Table 1: Weighted Demographics by Exposure ---
table1_vars <- c("age", "sex_binary", "edu_binary", "income_binary",
"smoking_3cat", "alcohol_3cat", "obesity_binary", "cvd_binary", "outcome")
table1_rows <- lapply(table1_vars, function(v) {
tryCatch({
if (is.numeric(df[[v]])) {
means <- svyby(as.formula(paste0("~", v)), ~exposure, design, svymean, na.rm = TRUE)
data.frame(variable = v,
unexposed_mean = means[means$exposure == 0, v],
exposed_mean = means[means$exposure == 1, v],
stringsAsFactors = FALSE)
} else {
props <- svyby(as.formula(paste0("~factor(", v, ")")), ~exposure, design, svymean, na.rm = TRUE)
data.frame(variable = paste0(v, "_", names(props)[-1]),
unexposed_mean = as.numeric(props[props$exposure == 0, -1]),
exposed_mean = as.numeric(props[props$exposure == 1, -1]),
stringsAsFactors = FALSE)
}
}, error = function(e) NULL)
})
table1 <- do.call(rbind, Filter(Negate(is.null), table1_rows))
write.csv(table1, file.path(RESULTS_DIR, "table1.csv"), row.names = FALSE)
# --- Helper: extract OR + CI + p from svyglm ---
extract_or <- function(model) {
or_val <- unname(exp(coef(model)["exposure"]))
ci_vals <- unname(exp(confint(model))["exposure", ])
p_val <- unname(summary(model)$coefficients["exposure", "Pr(>|t|)"])
c(OR = or_val, lower = ci_vals[1], upper = ci_vals[2], p = p_val)
}
# --- Main Analysis: Sequential Models ---
results <- list()
# Model 1: Unadjusted
m1 <- svyglm(outcome ~ exposure, design = design, family = quasibinomial())
results[["Model 1 (unadjusted)"]] <- extract_or(m1)
# Model 2: Age + Sex
m2 <- svyglm(outcome ~ exposure + age + sex_binary, design = design, family = quasibinomial())
results[["Model 2 (age+sex)"]] <- extract_or(m2)
# Model 3: Fully adjusted
formula_full <- as.formula(paste("outcome ~ exposure +", paste(COVARIATES, collapse = " + ")))
m3 <- svyglm(formula_full, design = design, family = quasibinomial())
results[["Model 3 (fully adjusted)"]] <- extract_or(m3)
# Save main results
main_df <- data.frame(
combo_id = COMBO_ID,
exposure = "{{EXPOSURE_LABEL}}",
outcome = "{{OUTCOME_LABEL}}",
model = names(results),
N = nrow(df),
events = sum(df$outcome == 1),
OR = sapply(results, `[`, "OR"),
lower = sapply(results, `[`, "lower"),
upper = sapply(results, `[`, "upper"),
p_value = sapply(results, `[`, "p"),
row.names = NULL
)
write.csv(main_df, file.path(RESULTS_DIR, "main_results.csv"), row.names = FALSE)
# --- Subgroup Analyses (Model 3 within strata) ---
subgroup_vars <- list(
sex = list(var = "sex_binary", levels = c(0, 1), labels = c("Male", "Female")),
age_grp = list(var = "age_group", levels = c("20-39", "40-59", "60+"), labels = NULL),
edu = list(var = "edu_binary", levels = c(0, 1), labels = c("Non-college", "College")),
income = list(var = "income_binary", levels = c(0, 1), labels = c("Lower", "Higher"))
)
# Create age groups
df$age_group <- cut(df$age, breaks = c(20, 40, 60, Inf), right = FALSE,
labels = c("20-39", "40-59", "60+"))
design <- update(design, age_group = df$age_group)
subgroup_results <- list()
for (sg_name in names(subgroup_vars)) {
sg <- subgroup_vars[[sg_name]]
for (lev in sg$levels) {
tryCatch({
sub_design <- subset(design, get(sg$var) == lev)
n_sub <- nrow(sub_design$variables)
n_events_sub <- sum(sub_design$variables$outcome == 1)
if (n_sub >= 30 && n_events_sub >= 5) {
# Reduced model for subgroups (fewer covariates to avoid convergence issues)
sg_covars <- setdiff(COVARIATES, sg$var)
sg_formula <- as.formula(paste("outcome ~ exposure +", paste(sg_covars, collapse = " + ")))
m_sg <- svyglm(sg_formula, design = sub_design, family = quasibinomial())
r_sg <- extract_or(m_sg)
subgroup_results[[length(subgroup_results) + 1]] <- data.frame(
combo_id = COMBO_ID,
exposure = "{{EXPOSURE_LABEL}}",
outcome = "{{OUTCOME_LABEL}}",
subgroup = sg_name,
level = as.character(lev),
N = n_sub,
events = n_events_sub,
OR = r_sg["OR"],
lower = r_sg["lower"],
upper = r_sg["upper"],
p_value = r_sg["p"],
row.names = NULL
)
}
}, error = function(e) {
message(sprintf("[%s] Subgroup %s=%s failed: %s", COMBO_ID, sg_name, lev, e$message))
})
}
}
if (length(subgroup_results) > 0) {
subgroup_df <- do.call(rbind, subgroup_results)
write.csv(subgroup_df, file.path(RESULTS_DIR, "subgroup_results.csv"), row.names = FALSE)
}
cat(sprintf("[%s] Complete. Fully adjusted wOR: %.2f (%.2f–%.2f), p=%.3f\n",
COMBO_ID,
main_df$OR[3], main_df$lower[3], main_df$upper[3], main_df$p_value[3]))
#!/usr/bin/env Rscript
# ==============================================================================
# Batch Cohort Analysis Generator
# Generates N analysis scripts from a base template × variable combinations
#
# Usage:
# Rscript batch_template_generator.R \
# --template base_template.R \
# --combinations combinations.csv \
# --outdir batch_20260417/ \
# --mode code_only|execute|full
#
# Combinations CSV columns:
# id, exposure_var, exposure_label, exposure_coding,
# outcome_var, outcome_label, outcome_coding,
# covariates_remove (comma-separated vars to drop from default set)
# ==============================================================================
suppressPackageStartupMessages({
library(optparse)
library(glue)
})
# --- CLI Arguments ---
option_list <- list(
make_option("--template", type = "character", help = "Path to base R template"),
make_option("--combinations", type = "character", help = "CSV with variable combinations"),
make_option("--outdir", type = "character", default = "batch_output", help = "Output directory"),
make_option("--mode", type = "character", default = "code_only",
help = "code_only | execute | full"),
make_option("--parallel", type = "integer", default = 1, help = "Number of parallel workers")
)
opt <- parse_args(OptionParser(option_list = option_list))
# --- Read Inputs ---
template_code <- readLines(opt$template)
combos <- read.csv(opt$combinations, stringsAsFactors = FALSE)
cat(glue("Loaded {nrow(combos)} combinations from {opt$combinations}\n\n"))
# --- Create Output Structure ---
dir.create(file.path(opt$outdir, "scripts"), recursive = TRUE, showWarnings = FALSE)
dir.create(file.path(opt$outdir, "template"), showWarnings = FALSE)
dir.create(file.path(opt$outdir, "logs"), showWarnings = FALSE)
# Freeze template
writeLines(template_code, file.path(opt$outdir, "template", "base_template.R"))
# --- Generate Scripts ---
script_paths <- character(nrow(combos))
for (i in seq_len(nrow(combos))) {
combo <- combos[i, ]
script_name <- sprintf("%02d_%s_%s.R", i, combo$exposure_var, combo$outcome_var)
script_path <- file.path(opt$outdir, "scripts", script_name)
# Results directory for this combination
results_dir <- file.path(opt$outdir, "results",
sprintf("%02d_%s_%s", i, combo$exposure_var, combo$outcome_var))
dir.create(results_dir, recursive = TRUE, showWarnings = FALSE)
# Replace slot variables in template
code <- template_code
code <- gsub("\\{\\{EXPOSURE_VAR\\}\\}", combo$exposure_var, code)
code <- gsub("\\{\\{EXPOSURE_LABEL\\}\\}", combo$exposure_label, code)
code <- gsub("\\{\\{EXPOSURE_CODING\\}\\}", combo$exposure_coding, code)
code <- gsub("\\{\\{OUTCOME_VAR\\}\\}", combo$outcome_var, code)
code <- gsub("\\{\\{OUTCOME_LABEL\\}\\}", combo$outcome_label, code)
code <- gsub("\\{\\{OUTCOME_CODING\\}\\}", combo$outcome_coding, code)
code <- gsub("\\{\\{RESULTS_DIR\\}\\}", results_dir, code)
code <- gsub("\\{\\{COMBO_ID\\}\\}", sprintf("%02d", i), code)
# Handle covariate removal (self-adjustment prevention)
if (!is.na(combo$covariates_remove) && nchar(combo$covariates_remove) > 0) {
remove_vars <- trimws(strsplit(combo$covariates_remove, ",")[[1]])
remove_str <- paste0('"', remove_vars, '"', collapse = ", ")
code <- gsub("\\{\\{COVARIATES_REMOVE\\}\\}", remove_str, code)
} else {
code <- gsub("\\{\\{COVARIATES_REMOVE\\}\\}", "", code)
}
writeLines(code, script_path)
script_paths[i] <- script_path
cat(glue(" Generated: {script_name}\n"))
}
# --- Generate Master Runner ---
runner_path <- file.path(opt$outdir, "scripts", "run_all.R")
runner_code <- c(
"#!/usr/bin/env Rscript",
"# Master runner — executes all batch scripts sequentially",
"# Auto-generated by batch_template_generator.R",
"",
'cat("=== Batch Cohort Analysis ===\\n")',
glue('cat("Combinations: {nrow(combos)}\\n")'),
glue('cat("Started: ", format(Sys.time(), "%Y-%m-%d %H:%M:%S"), "\\n\\n")'),
"",
"results_log <- data.frame(",
" id = character(), exposure = character(), outcome = character(),",
" status = character(), time_sec = numeric(), error_msg = character(),",
" stringsAsFactors = FALSE",
")",
"",
"scripts <- c(",
paste0(' "', script_paths, '"', collapse = ",\n"),
")",
"",
"for (i in seq_along(scripts)) {",
" cat(sprintf('\\n--- Running %d/%d: %s ---\\n', i, length(scripts), basename(scripts[i])))",
" t0 <- proc.time()['elapsed']",
" status <- tryCatch({",
" source(scripts[i], local = new.env())",
' "SUCCESS"',
" }, error = function(e) {",
' paste0("FAILED: ", conditionMessage(e))',
" })",
" elapsed <- round(proc.time()['elapsed'] - t0, 1)",
" is_ok <- startsWith(status, 'SUCCESS')",
" results_log <- rbind(results_log, data.frame(",
paste0(" id = sprintf('%02d', i),"),
" exposure = basename(scripts[i]),",
" outcome = '',",
' status = ifelse(is_ok, "SUCCESS", "FAILED"),',
" time_sec = elapsed,",
' error_msg = ifelse(is_ok, "", status),',
" stringsAsFactors = FALSE",
" ))",
" cat(sprintf(' %s (%.1fs)\\n', status, elapsed))",
"}",
"",
"# Save execution log",
glue('write.csv(results_log, "{file.path(opt$outdir, "logs", "batch_execution.csv")}", row.names = FALSE)'),
"",
'cat("\\n=== Batch Complete ===\\n")',
'cat(sprintf("Success: %d / %d\\n", sum(results_log$status == "SUCCESS"), nrow(results_log)))',
'cat(sprintf("Failed: %d / %d\\n", sum(results_log$status == "FAILED"), nrow(results_log)))'
)
writeLines(runner_code, runner_path)
cat(glue("\nMaster runner: {runner_path}\n"))
# --- Generate Summary Aggregator ---
aggregator_path <- file.path(opt$outdir, "scripts", "aggregate_results.R")
aggregator_code <- c(
"#!/usr/bin/env Rscript",
"# Aggregate results from all batch runs into summary matrix",
"",
glue('results_base <- "{file.path(opt$outdir, "results")}"'),
"dirs <- list.dirs(results_base, recursive = FALSE)",
"",
"all_main <- list()",
"all_subgroup <- list()",
"",
"for (d in dirs) {",
' main_file <- file.path(d, "main_results.csv")',
" if (file.exists(main_file)) {",
" df <- read.csv(main_file, stringsAsFactors = FALSE)",
" df$combo_id <- basename(d)",
" all_main[[length(all_main) + 1]] <- df",
" }",
' sub_file <- file.path(d, "subgroup_results.csv")',
" if (file.exists(sub_file)) {",
" df <- read.csv(sub_file, stringsAsFactors = FALSE)",
" df$combo_id <- basename(d)",
" all_subgroup[[length(all_subgroup) + 1]] <- df",
" }",
"}",
"",
"if (length(all_main) > 0) {",
" summary_matrix <- do.call(rbind, all_main)",
glue(' write.csv(summary_matrix, "{file.path(opt$outdir, "summary", "summary_matrix.csv")}", row.names = FALSE)'),
' cat(sprintf("Summary matrix: %d rows from %d combinations\\n", nrow(summary_matrix), length(all_main)))',
"}",
"",
"if (length(all_subgroup) > 0) {",
" subgroup_matrix <- do.call(rbind, all_subgroup)",
glue(' write.csv(subgroup_matrix, "{file.path(opt$outdir, "summary", "subgroup_matrix.csv")}", row.names = FALSE)'),
' cat(sprintf("Subgroup matrix: %d rows\\n", nrow(subgroup_matrix)))',
"}"
)
dir.create(file.path(opt$outdir, "summary"), showWarnings = FALSE)
writeLines(aggregator_code, aggregator_path)
cat(glue("Aggregator: {aggregator_path}\n"))
# --- Execute if requested ---
if (opt$mode %in% c("execute", "full")) {
cat("\n=== Executing batch ===\n")
source(runner_path)
cat("\n=== Aggregating results ===\n")
source(aggregator_path)
}
# --- Generate README ---
readme_path <- file.path(opt$outdir, "README.md")
readme <- c(
glue("# Batch Cohort Analysis — {Sys.Date()}"),
"",
glue("- **Template**: `{opt$template}`"),
glue("- **Combinations**: {nrow(combos)}"),
glue("- **Mode**: {opt$mode}"),
"",
"## Combination Matrix",
"",
"| # | Exposure | Outcome | Covariates Removed |",
"|---|----------|---------|-------------------|",
apply(combos, 1, function(row) {
sprintf("| %s | %s | %s | %s |",
row["id"], row["exposure_label"], row["outcome_label"],
ifelse(is.na(row["covariates_remove"]), "—", row["covariates_remove"]))
}),
"",
"## Notes",
"",
"- This is an **exploratory** analysis for hypothesis generation, not confirmation.",
"- Multiple comparisons: interpret p-values with caution (Bonferroni threshold shown in summary).",
"- Survey weights applied to all analyses.",
glue("- Data file SHA256: [compute with `shasum -a 256 <datafile>`]"),
""
)
writeLines(readme, readme_path)
cat(glue("\n✅ Batch generation complete: {opt$outdir}/\n"))
cat(glue(" {nrow(combos)} scripts generated\n"))
Variable Coding Registry
Pre-validated variable definitions for batch code generation. Each entry provides raw variable names, coding logic, and human-readable labels for KNHANES and NHANES. Add new variables as they are validated in replication studies.
Exposures
Depression (PHQ-9 ≥ 10)
| Field | KNHANES | NHANES |
|---|---|---|
| Raw vars | BP_PHQ_1 ~ BP_PHQ_9 | DPQ010 ~ DPQ090 |
| Coding | sum(BP_PHQ_1:9), NA if any missing | recode text→0-3, sum, NA if any missing |
| Binary | phq_score ≥ 10 → 1 | phq_score ≥ 10 → 1 |
| Label | Depression (PHQ-9 ≥ 10) | Depression (PHQ-9 ≥ 10) |
| Notes | Items are numeric 0-3 | Items are TEXT: "Not at all"→0, "Several days"→1, "More than half the days"→2, "Nearly every day"→3 |
Obesity
| Field | KNHANES | NHANES |
|---|---|---|
| Raw vars | HE_obe (or BMXBMI) | BMXBMI (BMX_J) |
| Coding | HE_obe ≥ 4 (Asian cutoff: BMI ≥ 25) | BMXBMI ≥ 30 (WHO cutoff) |
| Binary | obesity = 1 if HE_obe ≥ 4 | obesity = 1 if BMXBMI ≥ 30 |
| Label | Obesity (BMI ≥ 25, Asian) | Obesity (BMI ≥ 30, WHO) |
| Notes | 6-level: 1=underweight to 6=morbid obesity | Continuous BMI; use 25 for Asian-Americans analysis |
Current Smoking
| Field | KNHANES | NHANES |
|---|---|---|
| Raw vars | BS3_1 | SMQ020, SMQ040 |
| Coding | BS3_1 %in% c(1,2) → current; 3 → former; 8 → never | SMQ020=="Yes" & SMQ040 %in% c("Every day","Some days") → current |
| Binary | smoking_current = 1 if BS3_1 in (1,2) | smoking_current = 1 if above |
| Label | Current smoking | Current smoking |
| Notes | 1=daily, 2=occasional, 3=former, 8=never | Two-step: ≥100 lifetime cigs + currently smoke |
Heavy/Frequent Drinking
| Field | KNHANES | NHANES |
|---|---|---|
| Raw vars | BD1_11 | ALQ111, ALQ121 |
| Coding | BD1_11 in 2:6 → frequent; 1 → occasional; 8 → never | ALQ111=="No" → never; ALQ121 frequency mapping |
| Binary | drinking_freq = 1 if BD1_11 in 2:6 | drinking_freq = 1 if ALQ121 indicates monthly+ |
| Label | Frequent alcohol use | Frequent alcohol use |
| Notes | 1=past-year abstainer, 2-6=frequency levels, 8=lifetime never | ALQ121 text labels; never=ALQ111=="No" (ALQ121 is NA) |
Low Education
| Field | KNHANES | NHANES |
|---|---|---|
| Raw vars | edu | DMDEDUC2 |
| Coding | edu in 1:3 → non-college; 4 → college+ | "Less than 9th grade"/"9-11th grade"/"High school" → non-college |
| Binary | low_edu = 1 if edu in 1:3 | low_edu = 1 if non-college |
| Label | Non-college education | Non-college education |
Low Income
| Field | KNHANES | NHANES |
|---|---|---|
| Raw vars | incm | INDFMPIR (INQ_J) |
| Coding | incm in 1:3 → bottom 80% | INDFMPIR < 1.3 → low income |
| Binary | low_income = 1 if incm in 1:3 | low_income = 1 if PIR < 1.3 |
| Label | Lower income (bottom 80%) | Low income (PIR < 1.3) |
| Notes | 4-level quartile | Poverty income ratio; threshold varies by study |
Outcomes
Diabetes
| Field | KNHANES | NHANES |
|---|---|---|
| Raw vars | HE_glu, HE_HbA1c, DE1_dg | LBXSGL (BIOPRO_J), LBXGH (GHB_J), DIQ010 |
| Coding | HE_glu≥126 \ | HE_HbA1c≥6.5 \ |
| Label | Diabetes mellitus | Diabetes mellitus |
| Notes | FPG=fasting plasma glucose | LBXSGL not LBXSGLU; DIQ010 is text |
Hypertension
| Field | KNHANES | NHANES |
|---|---|---|
| Raw vars | HE_sbp, HE_dbp, DI1_dg | BPXOSY1-4, BPXODI1-4, BPQ020 |
| Coding | mean(SBP)≥140 \ | mean(DBP)≥90 \ |
| Label | Hypertension | Hypertension |
| Notes | HE_sbp/dbp are already averaged | Average of up to 4 readings; BPQ020 is text |
CVD History
| Field | KNHANES | NHANES |
|---|---|---|
| Raw vars | DI4_dg, DI5_dg, DI6_dg | MCQ160B, MCQ160C, MCQ160D |
| Coding | any == 1 → CVD | any == "Yes" → CVD |
| Label | Cardiovascular disease history | Cardiovascular disease history |
| Notes | 4=MI, 5=angina, 6=stroke | B=CHF, C=CHD, D=angina |
Metabolic Syndrome
| Field | KNHANES | NHANES |
|---|---|---|
| Raw vars | HE_wc, HE_sbp, HE_dbp, HE_TG, HE_HDL_st2, HE_glu | BMXWAIST, BPXOSY, BPXODI, LBXSTR, LBDHDD, LBXSGL |
| Coding | NCEP ATP III: ≥3 of 5 criteria | NCEP ATP III: ≥3 of 5 criteria |
| Label | Metabolic syndrome (NCEP ATP III) | Metabolic syndrome (NCEP ATP III) |
| Notes | Waist: M≥90, F≥85 (Korean) | Waist: M≥102, F≥88 (US/WHO) |
Standard Covariate Set
Default covariates for fully adjusted model (remove exposure if overlap):
| Covariate | KNHANES var | NHANES var | Type |
|---|---|---|---|
| Age | age | RIDAGEYR | Continuous |
| Sex | sex | RIAGENDR | Binary |
| Education | edu | DMDEDUC2 | Binary (college vs non) |
| Income | incm | INDFMPIR | Binary (threshold varies) |
| Smoking | BS3_1 | SMQ020+SMQ040 | 3-level (current/former/never) |
| Alcohol | BD1_11 | ALQ111+ALQ121 | 3-level (frequent/occasional/never) |
| Obesity | HE_obe | BMXBMI | Binary (country-specific cutoff) |
| CVD | DI4-6_dg | MCQ160B-D | Binary |
Survey Design
| Component | KNHANES | NHANES |
|---|---|---|
| Strata | kstrata | SDMVSTRA |
| PSU/Cluster | psu | SDMVPSU |
| Weight | wt_itvex | WTMEC2YR (single-cycle) or WTMECPRP (pooled) |
| R function | svydesign(id=~psu, strata=~kstrata, weights=~wt_itvex, nest=TRUE) | svydesign(id=~SDMVPSU, strata=~SDMVSTRA, weights=~WTMEC2YR, nest=TRUE) |
Adding New Variables
When a new variable is validated through /replicate-study or /cross-national: 1. Add an entry in the appropriate section (Exposure or Outcome) 2. Include both KNHANES and NHANES coding 3. Note any text-label gotchas for NHANES 4. Document the source paper that validated the coding
schema_version: 2
name: batch-cohort
layer: B
owner_domain: batch_analysis
maturity: official
when_to_use: "Generate N analysis scripts from one validated methodology template across many exposure/outcome combinations."
when_NOT_to_use: "A single analysis (use analyze-stats); cross-country comparison (use cross-national)."
inputs:
- "methodology template"
- "exposure/outcome combination matrix"
outputs:
- "N analysis scripts"
- "summary matrix"
side_effects:
- writes_project_artifacts
downstream_consumers:
- analyze-stats
- self-review
forbidden_actions:
- alter_methodology_per_combination_without_disclosure
- fabricate_results_matrix
# v2.1 quality card
purpose: "Scale one validated method across many variable combinations, swapping only exposure/outcome, never the method."
safety_boundaries:
- "The methodology is held constant across all generated scripts; deviations are disclosed."
- "Generated scripts run on real data; no results are pre-filled."
known_limitations:
- "Inherits the source template's assumptions; a flawed template propagates."
- "No standalone demo; outputs are code to be executed and reviewed."
validation_commands:
- "execute each generated script and reconcile the summary matrix"
- "/self-review"
evidence_surface: manual_workflow
Related skills
FAQ
What pattern does batch-cohort replicate?
The '80-person research team' pattern: one PI designs the methodology and many analyses execute it with different variable swaps.
What inputs does it need?
Database path(s), a validated methodology template, and a combination spec of exposure/outcome pairs.