# ── Input validation ─────────────────────────────────────────────────────────
missing_files <- CONDITION_FILES[!file.exists(CONDITION_FILES)]
if (length(missing_files) > 0) {
stop("CONDITION_FILES not found: ",
paste(sQuote(missing_files), collapse = ", "),
". Run `Rscript data/simulate_data.R` from the template directory to ",
"regenerate the demo data, or point CONDITION_FILES at your own CSVs.")
}
if (!FOCAL_CONDITION %in% names(CONDITION_FILES)) {
stop("FOCAL_CONDITION (", sQuote(FOCAL_CONDITION), ") is not one of ",
"names(CONDITION_FILES): ",
paste(sQuote(names(CONDITION_FILES)), collapse = ", "))
}
stopifnot(PSEUDOCOUNT >= 0, P_ADJUST_METHOD %in% p.adjust.methods)
clean_one <- function(path, condition_label) {
raw <- read_csv(path, show_col_types = FALSE) %>%
select(-matches("^Unnamed")) # drop blank index cols
if (ncol(raw) == 0) {
stop("No usable columns in ", sQuote(path),
" — expected one column per cell type / sample group.")
}
long <- raw %>%
pivot_longer(
everything(),
names_to = "cell_type_raw",
values_to = "value"
) %>%
filter(!is.na(value))
if (!is.numeric(long$value)) {
stop("Non-numeric values in ", sQuote(path),
". Every column must hold measurements only (no text, no units).")
}
if (any(long$value < 0)) {
stop("Negative values in ", sQuote(path),
" — these cannot be log-transformed. Inspect the file before proceeding.")
}
# log10(0) is -Inf, and -Inf is not NA, so it passes every is.na() guard and
# then breaks shapiro.test()/oneway.test()/t.test(). Refuse rather than let
# that happen silently.
if (LOG_TRANSFORM && PSEUDOCOUNT == 0 && any(long$value == 0)) {
stop(sum(long$value == 0), " zero value(s) in ", sQuote(path),
" with LOG_TRANSFORM = TRUE and PSEUDOCOUNT = 0. ",
"Set PSEUDOCOUNT to a positive number (e.g. 1, or your limit of detection).")
}
long %>%
mutate(
cell_type = str_remove(cell_type_raw, "\\.[0-9]+$") %>% # strip ".1"
str_remove("\\.+$"), # strip trailing dots
replicate = if_else(
str_detect(cell_type_raw, "\\.[0-9]+$"),
as.integer(str_extract(cell_type_raw, "\\d+$")) + 1L,
1L
),
condition = condition_label,
log_value = if (LOG_TRANSFORM) log10(value + PSEUDOCOUNT) else value
) %>%
select(condition, cell_type, replicate, value, log_value)
}
all_data <- imap_dfr(CONDITION_FILES, ~ clean_one(.x, .y))
# Nothing downstream can recover from a non-finite response.
stopifnot(all(is.finite(all_data$log_value)))
if (!REFERENCE_GROUP %in% all_data$cell_type[all_data$condition == FOCAL_CONDITION]) {
stop("REFERENCE_GROUP (", sQuote(REFERENCE_GROUP), ") is not present in ",
FOCAL_CONDITION, ". Available groups: ",
paste(sQuote(sort(unique(all_data$cell_type[all_data$condition == FOCAL_CONDITION]))),
collapse = ", "))
}
all_data %>%
head(15) %>%
knitr::kable(
caption = "Preview of Processed Condition Data (First 15 Rows)",
col.names = c("Condition", "Cell Type", "Replicate", "Raw Value", "Log10 Value"),
digits = 3,
format.args = list(big.mark = ",")
)