From b39123283df047124c49ce60b5630a37de729237 Mon Sep 17 00:00:00 2001 From: Emily Boyer Date: Wed, 12 Aug 2026 15:20:30 -0600 Subject: [PATCH 01/14] The stratification token appears right after drug in teh actual result filenames, not after the drug value so there was mislabeling. Corrected the token order to match was regex expects, and added regression tests for the stratified and unstratified cases. --- R/merge_ml_results.R | 16 ++++--- man/parse_ml_filename.Rd | 1 + tests/testthat/test-merge-ml-results.R | 61 ++++++++++++++++++++++++++ 3 files changed, 73 insertions(+), 5 deletions(-) create mode 100644 tests/testthat/test-merge-ml-results.R diff --git a/R/merge_ml_results.R b/R/merge_ml_results.R index ce32baf..6494f4f 100644 --- a/R/merge_ml_results.R +++ b/R/merge_ml_results.R @@ -21,6 +21,7 @@ #' #' @examples #' parse_ml_filename("Csp_drug_AMX_genes_binary_42_top_features.tsv") +#' parse_ml_filename("Csp_drug_year_AMX_2010-2015_genes_binary_year_42_performance.tsv") #' #' @export parse_ml_filename <- function(filename) { @@ -63,25 +64,30 @@ parse_ml_filename <- function(filename) { # Case A: drug_class if (xs[i + 1] == "class") { out$drug_label <- "drug_class" - out$drug_or_class <- xs[i + 2] - i <- i + 3 + i <- i + 2 } # Case B: simple drug else { out$drug_label <- "drug" - out$drug_or_class <- xs[i + 1] - i <- i + 2 + i <- i + 1 } } else { stop("ERROR: expected 'drug' token after species") } # --------------------------- - # 4. Stratified? + # 4. Stratified? (the strat label, if present, comes before the + # drug/drug_class value, e.g. "..._drug_year_AMX_2010-2015_...") # --------------------------- if (i <= length(xs) && xs[i] %in% c("year", "country")) { out$strat_label <- xs[i] i <- i + 1 + } + + out$drug_or_class <- xs[i] + i <- i + 1 + + if (!is.na(out$strat_label)) { out$strat_value <- xs[i] i <- i + 1 } diff --git a/man/parse_ml_filename.Rd b/man/parse_ml_filename.Rd index 8bd6ec2..7bd48e9 100644 --- a/man/parse_ml_filename.Rd +++ b/man/parse_ml_filename.Rd @@ -30,5 +30,6 @@ feature types, and seed information. } \examples{ parse_ml_filename("Csp_drug_AMX_genes_binary_42_top_features.tsv") +parse_ml_filename("Csp_drug_year_AMX_2010-2015_genes_binary_year_42_performance.tsv") } diff --git a/tests/testthat/test-merge-ml-results.R b/tests/testthat/test-merge-ml-results.R new file mode 100644 index 0000000..58cdde8 --- /dev/null +++ b/tests/testthat/test-merge-ml-results.R @@ -0,0 +1,61 @@ +# Unit tests for parse_ml_filename() in merge_ml_results.R. + +test_that("parse_ml_filename parses an unstratified drug filename", { + out <- parse_ml_filename("Csp_drug_AMX_genes_binary_42_top_features.tsv") + + expect_false(out$shuffled) + expect_equal(out$species, "Csp") + expect_equal(out$drug_label, "drug") + expect_equal(out$drug_or_class, "AMX") + expect_true(is.na(out$strat_label)) + expect_true(is.na(out$strat_value)) + expect_equal(out$feature_type, "genes") + expect_equal(out$feature_subtype, "binary") + expect_equal(out$seed, 42L) +}) + +test_that("parse_ml_filename parses an unstratified drug_class filename", { + out <- parse_ml_filename("Csp_drug_class_AMINOGLYCOSIDES_genes_binary_42_performance.tsv") + + expect_equal(out$drug_label, "drug_class") + expect_equal(out$drug_or_class, "AMINOGLYCOSIDES") + expect_true(is.na(out$strat_label)) + expect_equal(out$seed, 42L) +}) + +test_that("parse_ml_filename detects a shuffled run", { + out <- parse_ml_filename("shuffled_Csp_drug_AMX_genes_binary_42_top_features.tsv") + + expect_true(out$shuffled) + expect_equal(out$drug_or_class, "AMX") +}) + +test_that("parse_ml_filename parses a year-stratified drug filename", { + # The strat label sits between "drug" and the drug value in the actual + # filenames written by the matrix-generation code, e.g. + # "_drug_year___...". + out <- parse_ml_filename( + "Csp_drug_year_AMX_2010-2015_genes_binary_year_42_performance.tsv" + ) + + expect_equal(out$species, "Csp") + expect_equal(out$drug_label, "drug") + expect_equal(out$drug_or_class, "AMX") + expect_equal(out$strat_label, "year") + expect_equal(out$strat_value, "2010-2015") + expect_equal(out$feature_type, "genes") + expect_equal(out$feature_subtype, "binary") + expect_equal(out$seed, 42L) +}) + +test_that("parse_ml_filename parses a country-stratified drug_class filename", { + out <- parse_ml_filename( + "Csp_drug_class_country_AMINOGLYCOSIDES_USA_genes_binary_country_7_top_features.tsv" + ) + + expect_equal(out$drug_label, "drug_class") + expect_equal(out$drug_or_class, "AMINOGLYCOSIDES") + expect_equal(out$strat_label, "country") + expect_equal(out$strat_value, "USA") + expect_equal(out$seed, 7L) +}) From b7e9494fce1e5e6d41db55b26073f59f28f4aaea Mon Sep 17 00:00:00 2001 From: Abhirupa Ghosh <100681585+AbhirupaGhosh@users.noreply.github.com> Date: Tue, 1 Sep 2026 13:25:14 -0600 Subject: [PATCH 02/14] simplify parse_ml_filename() --- R/merge_ml_results.R | 48 +++++++++++++++++++++++--------------------- 1 file changed, 25 insertions(+), 23 deletions(-) diff --git a/R/merge_ml_results.R b/R/merge_ml_results.R index 6494f4f..01cc9ac 100644 --- a/R/merge_ml_results.R +++ b/R/merge_ml_results.R @@ -21,7 +21,7 @@ #' #' @examples #' parse_ml_filename("Csp_drug_AMX_genes_binary_42_top_features.tsv") -#' parse_ml_filename("Csp_drug_year_AMX_2010-2015_genes_binary_year_42_performance.tsv") +#' parse_ml_filename("Csp_drug_class_AMG_genes_binary_42_performance.tsv") #' #' @export parse_ml_filename <- function(filename) { @@ -34,8 +34,8 @@ parse_ml_filename <- function(filename) { species = NA_character_, drug_label = NA_character_, drug_or_class = NA_character_, - strat_label = NA_character_, - strat_value = NA_character_, + # strat_label = NA_character_, + # strat_value = NA_character_, feature_type = NA_character_, feature_subtype = NA_character_, seed = NA_integer_ @@ -75,26 +75,28 @@ parse_ml_filename <- function(filename) { stop("ERROR: expected 'drug' token after species") } - # --------------------------- - # 4. Stratified? (the strat label, if present, comes before the - # drug/drug_class value, e.g. "..._drug_year_AMX_2010-2015_...") - # --------------------------- - if (i <= length(xs) && xs[i] %in% c("year", "country")) { - out$strat_label <- xs[i] - i <- i + 1 - } + # # --------------------------- + # # 4. Stratified? (the strat label, if present, comes before the + # # drug/drug_class value, e.g. "..._drug_year_AMX_2010-2015_...") + # # --------------------------- + # if (i <= length(xs) && xs[i] %in% c("year", "country")) { + # out$strat_label <- xs[i] + # i <- i + 1 + # } - out$drug_or_class <- xs[i] - i <- i + 1 + # out$drug_or_class <- xs[i] + # i <- i + 1 - if (!is.na(out$strat_label)) { - out$strat_value <- xs[i] - i <- i + 1 - } + # if (!is.na(out$strat_label)) { + # out$strat_value <- xs[i] + # i <- i + 1 + # } # --------------------------- - # 5. Feature type + subtype + # 4. Feature type + subtype # --------------------------- + out$drug_or_class <- xs[i] + i <- i + 1 out$feature_type <- xs[i] i <- i + 1 out$feature_subtype <- xs[i] @@ -103,13 +105,13 @@ parse_ml_filename <- function(filename) { # --------------------------- # 6. Trailing strat label (mirror) # --------------------------- - if (!is.na(out$strat_label)) { - stopifnot(xs[i] == out$strat_label) - i <- i + 1 - } + # if (!is.na(out$strat_label)) { + # stopifnot(xs[i] == out$strat_label) + # i <- i + 1 + # } # --------------------------- - # 7. Seed + # 6. Seed # --------------------------- out$seed <- as.integer(xs[i]) From 75a7e19f60a5f193d3d1f2560ed740eef7e144b2 Mon Sep 17 00:00:00 2001 From: Abhirupa Ghosh <100681585+AbhirupaGhosh@users.noreply.github.com> Date: Tue, 1 Sep 2026 13:47:27 -0600 Subject: [PATCH 03/14] Simplify buildPerfPq and buildTopFeatsPq --- R/merge_ml_results.R | 71 ++++++++++++++++++++++++++------------------ 1 file changed, 42 insertions(+), 29 deletions(-) diff --git a/R/merge_ml_results.R b/R/merge_ml_results.R index 01cc9ac..227a5fa 100644 --- a/R/merge_ml_results.R +++ b/R/merge_ml_results.R @@ -123,7 +123,7 @@ parse_ml_filename <- function(filename) { #' Reads all `_top_features.tsv` files, parses metadata from filenames, #' combines them into a single table, and writes a Parquet file. #' -#' @param path Base directory containing ML results +#' @param top_feat_dir_path The directory containing ML top features files #' @param out_parquet Output file name #' @param compression Compression method (default: "zstd") #' @param verbose Logical; print progress @@ -137,7 +137,7 @@ parse_ml_filename <- function(filename) { #' #' @export buildTopFeatsPq <- function( - path, + top_feat_dir_path, # stratify_by = NULL, # LOO = FALSE, # MDR = FALSE, @@ -146,10 +146,10 @@ buildTopFeatsPq <- function( compression = "zstd", verbose = TRUE ) { - # if (!is.character(path) || length(path) != 1 || is.na(path) || nchar(path) == 0) { - # stop("`path` must be a non-empty character scalar.") - # } - # path <- normalizePath(path) + if (!is.character(top_feat_dir_path) || length(top_feat_dir_path) != 1 || is.na(top_feat_dir_path) || nchar(top_feat_dir_path) == 0) { + stop("`path` must be a non-empty character scalar.") + } + top_feat_dir_path <- normalizePath(top_feat_dir_path) # if (!is.null(stratify_by) && !stratify_by %in% c("year", "country")) { # stop("`stratify_by` must be NULL, 'year', or 'country'.") @@ -164,20 +164,27 @@ buildTopFeatsPq <- function( # ----------------------- # Resolve directories (ensures existence) # ----------------------- - paths <- createMLResultDir(path, - stratify_by = NULL, - LOO = FALSE, - MDR = FALSE, - cross_test = FALSE - ) - top_dir <- paths$ML_top_features + # paths <- createMLResultDir(path, + # stratify_by = NULL, + # LOO = FALSE, + # MDR = FALSE, + # cross_test = FALSE + # ) + # top_dir <- paths$ML_top_features files <- list.files( - top_dir, - pattern = "_top_features\\.tsv$", - full.names = TRUE, - recursive = TRUE + top_feat_dir_path, + pattern = "_top_features\\.tsv$", + full.names = TRUE, + recursive = TRUE +) + +if (length(files) == 0) { + stop( + "No files matching '_top_features.tsv' were found in: ", + normalizePath(top_feat_dir_path, mustWork = FALSE) ) +} if (!length(files)) { return(tibble::tibble()) @@ -199,7 +206,7 @@ buildTopFeatsPq <- function( dplyr::bind_cols(meta_tbl[rep(1, nrow(df)), ], df) }) - out_path <- file.path(top_dir, basename(out_parquet)) + out_path <- file.path(top_feat_dir_path, basename(out_parquet)) arrow::write_parquet(out, out_path, compression = compression) if (verbose) message("Wrote ", out_path) @@ -211,7 +218,6 @@ buildTopFeatsPq <- function( #' Reads all `_performance.tsv` files, parses metadata from filenames, #' combines them into a single table, and writes a Parquet output. #' -#' @inheritParams buildTopFeatsPq #' #' @return A tibble with metadata columns + performance metrics #' @@ -222,7 +228,7 @@ buildTopFeatsPq <- function( #' #' @export buildPerfPq <- function( - path, + perf_dir_path, # stratify_by = NULL, # LOO = FALSE, # MDR = FALSE, @@ -231,10 +237,10 @@ buildPerfPq <- function( compression = "zstd", verbose = TRUE ) { - if (!is.character(path) || length(path) != 1 || is.na(path) || nchar(path) == 0) { + if (!is.character(perf_dir_path) || length(perf_dir_path) != 1 || is.na(perf_dir_path) || nchar(perf_dir_path) == 0) { stop("`path` must be a non-empty character scalar.") } - path <- normalizePath(path) + perf_dir_path <- normalizePath(perf_dir_path) # if (!is.null(stratify_by) && !stratify_by %in% c("year", "country")) { # stop("`stratify_by` must be NULL, 'year', or 'country'.") @@ -249,19 +255,26 @@ buildPerfPq <- function( # ----------------------- # Resolve directories from your function (ensures they exist) # ----------------------- - paths <- createMLResultDir(path, - stratify_by = NULL, LOO = FALSE, - cross_test = FALSE, MDR = FALSE - ) - perf_dir <- paths$ML_performance + # paths <- createMLResultDir(path, + # stratify_by = NULL, LOO = FALSE, + # cross_test = FALSE, MDR = FALSE + # ) + # perf_dir <- paths$ML_performance files <- list.files( - perf_dir, + perf_dir_path, pattern = "_performance\\.tsv$", full.names = TRUE, recursive = TRUE ) +if (length(files) == 0) { + stop( + "No files matching '_performance.tsv' were found in: ", + normalizePath(perf_dir_path, mustWork = FALSE) + ) +} + if (!length(files)) { return(tibble::tibble()) } @@ -281,7 +294,7 @@ buildPerfPq <- function( dplyr::bind_cols(meta_tbl[rep(1, nrow(df)), ], df) }) - out_path <- file.path(perf_dir, basename(out_parquet)) + out_path <- file.path(perf_dir_path, basename(out_parquet)) arrow::write_parquet(out, out_path, compression = compression) if (verbose) message("Wrote ", out_path) From 244afe74bfd9c7bf7e7b938c23438c3896e49c8d Mon Sep 17 00:00:00 2001 From: Abhirupa Ghosh <100681585+AbhirupaGhosh@users.noreply.github.com> Date: Tue, 1 Sep 2026 14:32:04 -0600 Subject: [PATCH 04/14] Update the filename pattern to remove stratification type twice. --- R/run_ML.R | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/R/run_ML.R b/R/run_ML.R index bb9e534..878a152 100644 --- a/R/run_ML.R +++ b/R/run_ML.R @@ -1111,15 +1111,15 @@ if (nrow(files) == 0) { ) # Stratification suffix - strat_suffix <- if (is.null(stratify_by) || identical(stratify_by, "")) { - "" - } else { - switch(stratify_by, - "country" = "_country", - "year" = "_year", - stop("`stratify_by` must be NULL, 'year', or 'country'.") - ) - } + # strat_suffix <- if (is.null(stratify_by) || identical(stratify_by, "")) { + # "" + # } else { + # switch(stratify_by, + # "country" = "_country", + # "year" = "_year", + # stop("`stratify_by` must be NULL, 'year', or 'country'.") + # ) + # } # Auto naming for shuffled and PCA shuffle_tag <- if (isTRUE(shuffle_labels)) "shuffled_" else "" @@ -1199,8 +1199,8 @@ if (nrow(files) == 0) { } seed_tag <- paste0("_", seed) - # Final base filename: shuffled_ + [LOO_/cross_test_] + + _pcaXX + _year/_country - base <- paste0(shuffle_tag, config_prefix, output_prefix, pca_tag, strat_suffix, seed_tag) + # Final base filename: shuffled_ + [LOO_/cross_test_] + + _pcaXX + seed + base <- paste0(shuffle_tag, config_prefix, output_prefix, pca_tag, seed_tag) if (!is.null(res$performance_tibble)) { readr::write_tsv( From 8ee70b4ba8cf890f5dc7bc6d8e613eff185597aa Mon Sep 17 00:00:00 2001 From: Abhirupa Ghosh <100681585+AbhirupaGhosh@users.noreply.github.com> Date: Tue, 1 Sep 2026 14:40:00 -0600 Subject: [PATCH 05/14] Save the performance and top features as parquets instead of tsv --- R/run_ML.R | 27 ++++++++++++++++----------- 1 file changed, 16 insertions(+), 11 deletions(-) diff --git a/R/run_ML.R b/R/run_ML.R index 878a152..014aee2 100644 --- a/R/run_ML.R +++ b/R/run_ML.R @@ -806,17 +806,20 @@ runMDRmodels <- function(path, base <- paste0(shuffle_tag, output_prefix, pca_tag, seed_tag) if (!is.null(res$performance_tibble)) { - readr::write_tsv( + arrow::write_parquet( res$performance_tibble, - file.path(files$out_perf[i], paste0(base, "_performance.tsv")) + file.path(files$out_perf[i], paste0(base, "_performance.parquet")), + compression = "zstd" ) } if (!is.null(res$top_feat_tibble)) { - readr::write_tsv( + arrow::write_parquet( res$top_feat_tibble, - file.path(files$out_top[i], paste0(base, "_top_features.tsv")) + file.path(files$out_top[i], paste0(base, "_top_features.parquet")), + compression = "zstd" ) } + if (!is.null(res$fit)) { saveRDS(res$fit, file.path(files$out_models[i], paste0(base, "_model_fit.rds"))) } @@ -916,7 +919,7 @@ runMDRmodels <- function(path, #' \item Stratification: Suffixed with \code{"_country"} or \code{"_year"} #' } #' -#' For example: \code{"LOO_cross_test_ML_year_performance.tsv"} +#' For example: \code{"LOO_cross_test_ML_year_performance.parquet"} #' #' @note #' This function requires the following packages: @@ -1050,8 +1053,8 @@ runMLmodels <- function(path, } # ---- strip stratification BEFORE seed ---- - perf_base <- sub("_(country|year)_([0-9]+)_performance\\.tsv$", - "_\\2_performance.tsv", + perf_base <- sub("_(country|year)_([0-9]+)_performance\\.parquet$", + "_\\2_performance.parquet", perf_base) # ---- final prefixes that ran ---- @@ -1203,17 +1206,19 @@ if (nrow(files) == 0) { base <- paste0(shuffle_tag, config_prefix, output_prefix, pca_tag, seed_tag) if (!is.null(res$performance_tibble)) { - readr::write_tsv( + arrow::write_parquet( res$performance_tibble, - file.path(files$out_perf[i], paste0(base, "_performance.tsv")) + file.path(files$out_perf[i], paste0(base, "_performance.parquet")) ) } + if (!is.null(res$top_feat_tibble)) { - readr::write_tsv( + arrow::write_parquet( res$top_feat_tibble, - file.path(files$out_top[i], paste0(base, "_top_features.tsv")) + file.path(files$out_top[i], paste0(base, "_top_features.parquet")) ) } + if (!is.null(res$fit)) { saveRDS(res$fit, file.path(files$out_models[i], paste0(base, "_model_fit.rds"))) } From 493e8acebcfd84c8d614ecdf0d55a562a4390238 Mon Sep 17 00:00:00 2001 From: Abhirupa Ghosh <100681585+AbhirupaGhosh@users.noreply.github.com> Date: Tue, 1 Sep 2026 14:41:47 -0600 Subject: [PATCH 06/14] Update tsv to parquet --- R/run_ML.R | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/R/run_ML.R b/R/run_ML.R index 014aee2..2ed5be0 100644 --- a/R/run_ML.R +++ b/R/run_ML.R @@ -1018,7 +1018,7 @@ runMLmodels <- function(path, # ---- performance files ---- perf_files <- list.files( path = unique(files$out_perf), - pattern = "_performance\\.tsv$", + pattern = "_performance\\.parquet$", full.names = FALSE ) @@ -1045,7 +1045,7 @@ runMLmodels <- function(path, perf_base <- sub("^cross_test_", "", perf_base) # ---- keep only this seed ---- - seed_pattern <- paste0("_", seed, "_performance\\.tsv$") + seed_pattern <- paste0("_", seed, "_performance\\.parquet$") perf_base <- perf_base[grepl(seed_pattern, perf_base)] if (length(perf_base) == 0) { From 8d4a0718285817d624ed80faae02c6e0a09f170e Mon Sep 17 00:00:00 2001 From: Abhirupa Ghosh <100681585+AbhirupaGhosh@users.noreply.github.com> Date: Tue, 1 Sep 2026 15:47:22 -0600 Subject: [PATCH 07/14] Update tsv to parquet for merging perf and top feats --- R/merge_ml_results.R | 99 ++++++++++++++++++++------------------------ R/run_ML.R | 16 ++++--- 2 files changed, 56 insertions(+), 59 deletions(-) diff --git a/R/merge_ml_results.R b/R/merge_ml_results.R index 227a5fa..a295595 100644 --- a/R/merge_ml_results.R +++ b/R/merge_ml_results.R @@ -20,13 +20,13 @@ #' } #' #' @examples -#' parse_ml_filename("Csp_drug_AMX_genes_binary_42_top_features.tsv") -#' parse_ml_filename("Csp_drug_class_AMG_genes_binary_42_performance.tsv") +#' parse_ml_filename("Csp_drug_AMX_genes_binary_42_top_features.parquet") +#' parse_ml_filename("Csp_drug_class_AMG_genes_binary_42_performance.parquet") #' #' @export parse_ml_filename <- function(filename) { # Strip known suffix - base <- sub("_(top_features|performance)\\.tsv$", "", filename) + base <- sub("_(top_features|performance)\\.parquet$", "", filename) xs <- strsplit(base, "_", fixed = TRUE)[[1]] out <- list( @@ -120,7 +120,7 @@ parse_ml_filename <- function(filename) { #' Build Parquet file of top ML features #' -#' Reads all `_top_features.tsv` files, parses metadata from filenames, +#' Reads all `_top_features.parquet` files, parses metadata from filenames, #' combines them into a single table, and writes a Parquet file. #' #' @param top_feat_dir_path The directory containing ML top features files @@ -174,14 +174,14 @@ buildTopFeatsPq <- function( files <- list.files( top_feat_dir_path, - pattern = "_top_features\\.tsv$", + pattern = "_top_features\\.parquet$", full.names = TRUE, recursive = TRUE ) if (length(files) == 0) { stop( - "No files matching '_top_features.tsv' were found in: ", + "No files matching '_top_features.parquet' were found in: ", normalizePath(top_feat_dir_path, mustWork = FALSE) ) } @@ -191,20 +191,13 @@ if (length(files) == 0) { } out <- purrr::map_dfr(files, function(f) { - meta <- parse_ml_filename(basename(f)) - df <- readr::read_tsv(f, - col_types = readr::cols( - Variable = readr::col_character(), - Importance = readr::col_double(), - Sign = readr::col_character() - ), - show_col_types = FALSE, - na = c("NA", "", "NaN") - ) + meta <- parse_ml_filename(basename(f)) - meta_tbl <- tibble::as_tibble(meta) - dplyr::bind_cols(meta_tbl[rep(1, nrow(df)), ], df) - }) + df <- arrow::read_parquet(f) + + meta_tbl <- tibble::as_tibble(meta) + dplyr::bind_cols(meta_tbl[rep(1, nrow(df)), ], df) +}) out_path <- file.path(top_feat_dir_path, basename(out_parquet)) arrow::write_parquet(out, out_path, compression = compression) @@ -215,7 +208,7 @@ if (length(files) == 0) { #' Build Parquet file of ML performance results #' -#' Reads all `_performance.tsv` files, parses metadata from filenames, +#' Reads all `_performance.parquet` files, parses metadata from filenames, #' combines them into a single table, and writes a Parquet output. #' #' @@ -263,14 +256,14 @@ buildPerfPq <- function( files <- list.files( perf_dir_path, - pattern = "_performance\\.tsv$", + pattern = "_performance\\.parquet$", full.names = TRUE, recursive = TRUE ) if (length(files) == 0) { stop( - "No files matching '_performance.tsv' were found in: ", + "No files matching '_performance.parquet' were found in: ", normalizePath(perf_dir_path, mustWork = FALSE) ) } @@ -281,7 +274,7 @@ if (length(files) == 0) { out <- purrr::map_dfr(files, function(f) { meta <- parse_ml_filename(basename(f)) - df <- readr::read_tsv(f, show_col_types = FALSE) + df <- arrow::read_parquet(f) # ---- SAFETY FIX ---- # If TSV already has seed, drop parsed seed @@ -318,12 +311,12 @@ if (length(files) == 0) { buildPerfPqYearCountry <- function( perf_dir_path ) { - files <- list.files(perf_dir_path, pattern = "\\.tsv$", full.names = TRUE) + files <- list.files(perf_dir_path, pattern = "\\.parquet$", full.names = TRUE) # Read and combine merged_df <- files |> purrr::set_names() |> # keeps file names attached - purrr::map_dfr(~ readr::read_tsv(.x, show_col_types = FALSE) |> + purrr::map_dfr(~ arrow::read_parquet(.x) |> dplyr::mutate(filename = basename(.x))) |> tidyr::extract( filename, @@ -338,7 +331,7 @@ buildPerfPqYearCountry <- function( "strat_label2", "seed_from_name" ), - regex = "^([^_]+)_((?:[^_]+(?:_[^_]+)?))_([^_]+)_([^_]+)_([^_]+)_([^_]+)_([^_]+)_([^_]+)_([^_]+)_performance\\.tsv$", + regex = "^([^_]+)_((?:[^_]+(?:_[^_]+)?))_([^_]+)_([^_]+)_([^_]+)_([^_]+)_([^_]+)_([^_]+)_([^_]+)_performance\\.parquet$", remove = FALSE ) |> dplyr::select(-c("strat_label2", "seed_from_name")) @@ -365,12 +358,12 @@ buildPerfPqYearCountry <- function( buildPerfPqCrossDrug <- function( perf_dir_path ) { - files <- list.files(perf_dir_path, pattern = "\\.tsv$", full.names = TRUE) + files <- list.files(perf_dir_path, pattern = "\\.parquet$", full.names = TRUE) # Read and combine merged_df <- files |> purrr::set_names() |> # keeps file names attached - purrr::map_dfr(~ readr::read_tsv(.x, show_col_types = FALSE) |> + purrr::map_dfr(~ arrow::read_parquet(.x) |> dplyr::mutate(filename = basename(.x))) |> tidyr::extract( filename, @@ -396,12 +389,12 @@ buildPerfPqCrossDrug <- function( buildPerfPqCrossYear <- function( perf_dir_path ) { - files <- list.files(perf_dir_path, pattern = "\\.tsv$", full.names = TRUE) + files <- list.files(perf_dir_path, pattern = "\\.parquet$", full.names = TRUE) # Read and combine merged_df <- files |> purrr::set_names() |> # keeps file names attached - purrr::map_dfr(~ readr::read_tsv(.x, show_col_types = FALSE) |> + purrr::map_dfr(~ arrow::read_parquet(.x) |> dplyr::mutate(filename = basename(.x))) |> tidyr::extract( filename, @@ -427,12 +420,12 @@ buildPerfPqCrossYear <- function( buildPerfPqCrossCountry <- function( perf_dir_path ) { - files <- list.files(perf_dir_path, pattern = "\\.tsv$", full.names = TRUE) + files <- list.files(perf_dir_path, pattern = "\\.parquet$", full.names = TRUE) # Read and combine merged_df <- files |> purrr::set_names() |> # keeps file names attached - purrr::map_dfr(~ readr::read_tsv(.x, show_col_types = FALSE) |> + purrr::map_dfr(~ arrow::read_parquet(.x) |> dplyr::mutate(filename = basename(.x))) |> tidyr::extract( filename, @@ -459,12 +452,12 @@ buildPerfPqCrossCountry <- function( buildPerfPqLOODrug <- function( perf_dir_path ) { - files <- list.files(perf_dir_path, pattern = "\\.tsv$", full.names = TRUE) + files <- list.files(perf_dir_path, pattern = "\\.parquet$", full.names = TRUE) # Read and combine merged_df <- files |> purrr::set_names() |> # keeps file names attached - purrr::map_dfr(~ readr::read_tsv(.x, show_col_types = FALSE) |> + purrr::map_dfr(~ arrow::read_parquet(.x) |> dplyr::mutate(filename = basename(.x))) |> tidyr::extract( filename, @@ -494,12 +487,12 @@ buildPerfPqLOODrug <- function( buildTopFeatsPqYearCountry <- function( top_feat_dir_path ) { - files <- list.files(top_feat_dir_path, pattern = "\\.tsv$", full.names = TRUE) + files <- list.files(top_feat_dir_path, pattern = "\\.parquet$", full.names = TRUE) # Read and combine merged_df <- files |> purrr::set_names() |> - purrr::map_dfr(~ readr::read_tsv(.x, show_col_types = FALSE) |> + purrr::map_dfr(~ arrow::read_parquet(.x) |> dplyr::mutate( Importance = as.numeric(Importance), filename = basename(.x) @@ -517,7 +510,7 @@ buildTopFeatsPqYearCountry <- function( "strat_label2", "seed_from_name" ), - regex = "^([^_]+)_((?:[^_]+(?:_[^_]+)?))_([^_]+)_([^_]+)_([^_]+)_([^_]+)_([^_]+)_([^_]+)_([^_]+)_top_features\\.tsv$", + regex = "^([^_]+)_((?:[^_]+(?:_[^_]+)?))_([^_]+)_([^_]+)_([^_]+)_([^_]+)_([^_]+)_([^_]+)_([^_]+)_top_features\\.parquet$", remove = FALSE ) |> dplyr::select(-c("strat_label2")) @@ -545,12 +538,12 @@ buildTopFeatsPqYearCountry <- function( buildTopFeatsPqLOODrug <- function( top_feat_dir_path ) { - files <- list.files(top_feat_dir_path, pattern = "\\.tsv$", full.names = TRUE) + files <- list.files(top_feat_dir_path, pattern = "\\.parquet$", full.names = TRUE) # Read and combine merged_df <- files |> purrr::set_names() |> # keeps file names attached - purrr::map_dfr(~ readr::read_tsv(.x, show_col_types = FALSE) |> + purrr::map_dfr(~ arrow::read_parquet(.x) |> dplyr::mutate(filename = basename(.x))) |> tidyr::extract( filename, @@ -577,12 +570,12 @@ buildTopFeatsPqLOODrug <- function( buildTopFeatsPqMDR <- function( top_feat_dir_path) { - files <- list.files(top_feat_dir_path, pattern = "\\.tsv$", full.names = TRUE) + files <- list.files(top_feat_dir_path, pattern = "\\.parquet$", full.names = TRUE) # Read and combine merged_df <- files|> purrr::set_names() |> # keeps file names attached - purrr::map_dfr(~ readr::read_tsv(.x, show_col_types = FALSE) |> + purrr::map_dfr(~ arrow::read_parquet(.x) |> dplyr::mutate(filename = basename(.x))) |> tidyr::extract( filename, @@ -611,12 +604,12 @@ buildTopFeatsPqMDR <- function( buildPerfPqMDR <- function( perf_dir_path) { - files <- list.files(perf_dir_path, pattern = "\\.tsv$", full.names = TRUE) + files <- list.files(perf_dir_path, pattern = "\\.parquet$", full.names = TRUE) # Read and combine merged_df <- files|> purrr::set_names() |> # keeps file names attached - purrr::map_dfr(~ readr::read_tsv(.x, show_col_types = FALSE) |> + purrr::map_dfr(~ arrow::read_parquet(.x) |> dplyr::mutate(filename = basename(.x))) |> tidyr::extract( filename, @@ -645,12 +638,12 @@ buildPerfPqMDR <- function( buildPredPqMDR <- function( pred_dir_path) { - files <- list.files(pred_dir_path, pattern = "\\.tsv$", full.names = TRUE) + files <- list.files(pred_dir_path, pattern = "\\.parquet$", full.names = TRUE) # Read and combine merged_df <- files|> purrr::set_names() |> # keeps file names attached - purrr::map_dfr(~ readr::read_tsv(.x, show_col_types = FALSE) |> + purrr::map_dfr(~ arrow::read_parquet(.x) |> dplyr::select(1:resistant_classes)|> dplyr::mutate(filename = basename(.x)) ) |> tidyr::extract( @@ -678,12 +671,12 @@ buildPredPqMDR <- function( buildPerfPqLOOCountry <- function( perf_dir_path ) { - files <- list.files(perf_dir_path, pattern = "\\.tsv$", full.names = TRUE) + files <- list.files(perf_dir_path, pattern = "\\.parquet$", full.names = TRUE) # Read and combine merged_df <- files |> purrr::set_names() |> # keeps file names attached - purrr::map_dfr(~ readr::read_tsv(.x, show_col_types = FALSE) |> + purrr::map_dfr(~ arrow::read_parquet(.x) |> dplyr::mutate(filename = basename(.x))) |> tidyr::extract( filename, @@ -709,12 +702,12 @@ buildPerfPqLOOCountry <- function( buildPerfPqLOOYear <- function( perf_dir_path ) { - files <- list.files(perf_dir_path, pattern = "\\.tsv$", full.names = TRUE) + files <- list.files(perf_dir_path, pattern = "\\.parquet$", full.names = TRUE) # Read and combine merged_df <- files |> purrr::set_names() |> # keeps file names attached - purrr::map_dfr(~ readr::read_tsv(.x, show_col_types = FALSE) |> + purrr::map_dfr(~ arrow::read_parquet(.x) |> dplyr::mutate(filename = basename(.x))) |> tidyr::extract( filename, @@ -742,12 +735,12 @@ buildPerfPqLOOYear <- function( buildTopFeatsPqLOOCountry <- function( top_feat_dir_path ) { - files <- list.files(top_feat_dir_path, pattern = "\\.tsv$", full.names = TRUE) + files <- list.files(top_feat_dir_path, pattern = "\\.parquet$", full.names = TRUE) # Read and combine merged_df <- files |> purrr::set_names() |> # keeps file names attached - purrr::map_dfr(~ readr::read_tsv(.x, show_col_types = FALSE) |> + purrr::map_dfr(~ arrow::read_parquet(.x) |> dplyr::mutate(filename = basename(.x))) |> tidyr::extract( filename, @@ -775,12 +768,12 @@ buildTopFeatsPqLOOCountry <- function( buildTopFeatsPqLOOYear <- function( top_feat_dir_path ) { - files <- list.files(top_feat_dir_path, pattern = "\\.tsv$", full.names = TRUE) + files <- list.files(top_feat_dir_path, pattern = "\\.parquet$", full.names = TRUE) # Read and combine merged_df <- files |> purrr::set_names() |> # keeps file names attached - purrr::map_dfr(~ readr::read_tsv(.x, show_col_types = FALSE) |> + purrr::map_dfr(~ arrow::read_parquet(.x) |> dplyr::mutate(filename = basename(.x))) |> tidyr::extract( filename, diff --git a/R/run_ML.R b/R/run_ML.R index 2ed5be0..f778234 100644 --- a/R/run_ML.R +++ b/R/run_ML.R @@ -824,9 +824,10 @@ runMDRmodels <- function(path, saveRDS(res$fit, file.path(files$out_models[i], paste0(base, "_model_fit.rds"))) } if (!is.null(res$pred)) { - readr::write_tsv( + arrow::write_parquet( res$pred, - file.path(files$out_pred[i], paste0(base, "_prediction.tsv")) + file.path(files$out_pred[i], paste0(base, "_prediction.parquet")), + compression = "zstd" ) } @@ -1208,14 +1209,16 @@ if (nrow(files) == 0) { if (!is.null(res$performance_tibble)) { arrow::write_parquet( res$performance_tibble, - file.path(files$out_perf[i], paste0(base, "_performance.parquet")) + file.path(files$out_perf[i], paste0(base, "_performance.parquet")), + compression = "zstd" ) } if (!is.null(res$top_feat_tibble)) { arrow::write_parquet( res$top_feat_tibble, - file.path(files$out_top[i], paste0(base, "_top_features.parquet")) + file.path(files$out_top[i], paste0(base, "_top_features.parquet")), + compression = "zstd" ) } @@ -1223,9 +1226,10 @@ if (nrow(files) == 0) { saveRDS(res$fit, file.path(files$out_models[i], paste0(base, "_model_fit.rds"))) } if (!is.null(res$pred)) { - readr::write_tsv( + arrow::write_parquet( res$pred, - file.path(files$out_pred[i], paste0(base, "_prediction.tsv")) + file.path(files$out_pred[i], paste0(base, "_prediction.parquet")), + compression = "zstd" ) } From f2c3b502afdb1203eb88140592f055ce6cfbecf6 Mon Sep 17 00:00:00 2001 From: Abhirupa Ghosh <100681585+AbhirupaGhosh@users.noreply.github.com> Date: Tue, 1 Sep 2026 16:17:45 -0600 Subject: [PATCH 08/14] clean the file names --- R/merge_ml_results.R | 15 ++++++++------- R/run_ML.R | 2 +- 2 files changed, 9 insertions(+), 8 deletions(-) diff --git a/R/merge_ml_results.R b/R/merge_ml_results.R index a295595..8d302a4 100644 --- a/R/merge_ml_results.R +++ b/R/merge_ml_results.R @@ -328,19 +328,20 @@ buildPerfPqYearCountry <- function( "strat_value", "feature_type", "feature_subtype", - "strat_label2", + # "strat_label2", "seed_from_name" ), - regex = "^([^_]+)_((?:[^_]+(?:_[^_]+)?))_([^_]+)_([^_]+)_([^_]+)_([^_]+)_([^_]+)_([^_]+)_([^_]+)_performance\\.parquet$", + regex = "^([^_]+)_((?:[^_]+(?:_[^_]+)?))_([^_]+)_([^_]+)_([^_]+)_([^_]+)_([^_]+)_([^_]+)_performance\\.parquet$", remove = FALSE ) |> - dplyr::select(-c("strat_label2", "seed_from_name")) + dplyr::select(-c("seed_from_name")) strat_label <- merged_df |> dplyr::distinct(strat_label) |> dplyr::pull() arrow::write_parquet(merged_df, file.path(perf_dir_path, paste0(strat_label, "_perf.parquet"))) + merged_df } #' Build Parquet file from cross drug testing ML performances @@ -507,13 +508,13 @@ buildTopFeatsPqYearCountry <- function( "strat_value", "feature_type", "feature_subtype", - "strat_label2", + # "strat_label2", "seed_from_name" ), - regex = "^([^_]+)_((?:[^_]+(?:_[^_]+)?))_([^_]+)_([^_]+)_([^_]+)_([^_]+)_([^_]+)_([^_]+)_([^_]+)_top_features\\.parquet$", + regex = "^([^_]+)_((?:[^_]+(?:_[^_]+)?))_([^_]+)_([^_]+)_([^_]+)_([^_]+)_([^_]+)_([^_]+)_top_features\\.parquet$", remove = FALSE - ) |> - dplyr::select(-c("strat_label2")) + ) + strat_label <- merged_df |> dplyr::distinct(strat_label) |> diff --git a/R/run_ML.R b/R/run_ML.R index f778234..0b834f9 100644 --- a/R/run_ML.R +++ b/R/run_ML.R @@ -1054,7 +1054,7 @@ runMLmodels <- function(path, } # ---- strip stratification BEFORE seed ---- - perf_base <- sub("_(country|year)_([0-9]+)_performance\\.parquet$", + perf_base <- sub("_([0-9]+)_performance\\.parquet$", "_\\2_performance.parquet", perf_base) From 643350c45d64f3b10437a29fcb7998a61b52fa1c Mon Sep 17 00:00:00 2001 From: Abhirupa Ghosh <100681585+AbhirupaGhosh@users.noreply.github.com> Date: Tue, 1 Sep 2026 17:30:44 -0600 Subject: [PATCH 09/14] Clean code --- R/merge_ml_results.R | 2 +- R/run_ML.R | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/R/merge_ml_results.R b/R/merge_ml_results.R index 8d302a4..5da7068 100644 --- a/R/merge_ml_results.R +++ b/R/merge_ml_results.R @@ -231,7 +231,7 @@ buildPerfPq <- function( verbose = TRUE ) { if (!is.character(perf_dir_path) || length(perf_dir_path) != 1 || is.na(perf_dir_path) || nchar(perf_dir_path) == 0) { - stop("`path` must be a non-empty character scalar.") + stop("`perf_dir_path` must be a non-empty character scalar.") } perf_dir_path <- normalizePath(perf_dir_path) diff --git a/R/run_ML.R b/R/run_ML.R index 0b834f9..dc90a43 100644 --- a/R/run_ML.R +++ b/R/run_ML.R @@ -1009,7 +1009,8 @@ runMLmodels <- function(path, .findNonRanPrefixes <- function(files, seed, - shuffle_labels = FALSE) { + shuffle_labels = FALSE) + { # ---- matrix prefixes ---- matrix_prefixes <- unique( From d48711e2dd957016f5e69ad0e5b16ab57bd95e0e Mon Sep 17 00:00:00 2001 From: Abhirupa Ghosh <100681585+AbhirupaGhosh@users.noreply.github.com> Date: Wed, 2 Sep 2026 09:45:55 -0600 Subject: [PATCH 10/14] update documentation --- DESCRIPTION | 2 +- NAMESPACE | 248 ++++++++++++++++++++++----------------- R/merge_ml_results.R | 6 +- man/buildPerfPq.Rd | 15 +-- man/buildTopFeatsPq.Rd | 8 +- man/parse_ml_filename.Rd | 6 +- man/runMLmodels.Rd | 2 +- 7 files changed, 156 insertions(+), 131 deletions(-) diff --git a/DESCRIPTION b/DESCRIPTION index 4191ecb..fc5cc37 100644 --- a/DESCRIPTION +++ b/DESCRIPTION @@ -89,4 +89,4 @@ biocViews: Visualization URL: https://github.com/JRaviLab/amRml BugReports: https://github.com/JRaviLab/amRml/issues -Config/roxygen2/version: 8.0.0 +Config/roxygen2/version: 8.1.0 diff --git a/NAMESPACE b/NAMESPACE index b43c4bb..334ddce 100644 --- a/NAMESPACE +++ b/NAMESPACE @@ -68,116 +68,154 @@ import(ggrepel) import(purrr) import(stringr) import(tibble) -importFrom(DBI,dbConnect) -importFrom(DBI,dbDisconnect) -importFrom(DBI,dbExecute) -importFrom(DBI,dbGetQuery) -importFrom(DBI,dbWriteTable) -importFrom(arrow,read_parquet) -importFrom(arrow,write_parquet) -importFrom(dplyr,all_of) -importFrom(dplyr,arrange) -importFrom(dplyr,bind_rows) -importFrom(dplyr,case_when) -importFrom(dplyr,count) -importFrom(dplyr,desc) -importFrom(dplyr,distinct) -importFrom(dplyr,filter) -importFrom(dplyr,group_by) -importFrom(dplyr,if_any) -importFrom(dplyr,matches) -importFrom(dplyr,mutate) -importFrom(dplyr,n_distinct) -importFrom(dplyr,pull) -importFrom(dplyr,relocate) -importFrom(dplyr,rename) -importFrom(dplyr,row_number) -importFrom(dplyr,rowwise) -importFrom(dplyr,select) -importFrom(dplyr,slice) -importFrom(dplyr,summarise) -importFrom(dplyr,ungroup) +importFrom(DBI, + dbConnect, + dbDisconnect, + dbExecute, + dbGetQuery, + dbWriteTable +) +importFrom(arrow, + read_parquet, + write_parquet +) +importFrom(dplyr, + all_of, + arrange, + bind_rows, + case_when, + count, + desc, + distinct, + filter, + group_by, + if_any, + matches, + mutate, + n_distinct, + pull, + relocate, + rename, + row_number, + rowwise, + select, + slice, + summarise, + ungroup +) importFrom(duckdb,duckdb) -importFrom(ggplot2,aes) -importFrom(ggplot2,element_blank) -importFrom(ggplot2,element_line) -importFrom(ggplot2,element_text) -importFrom(ggplot2,geom_line) -importFrom(ggplot2,geom_path) -importFrom(ggplot2,geom_point) -importFrom(ggplot2,ggplot) -importFrom(ggplot2,labs) -importFrom(ggplot2,theme) -importFrom(ggplot2,xlab) -importFrom(ggplot2,ylim) +importFrom(ggplot2, + aes, + element_blank, + element_line, + element_text, + geom_line, + geom_path, + geom_point, + ggplot, + labs, + theme, + xlab, + ylim +) importFrom(glmnet,glmnet) importFrom(glue,glue) importFrom(grDevices,colorRampPalette) importFrom(graphics,barplot) importFrom(hardhat,tune) -importFrom(jsonlite,fromJSON) -importFrom(jsonlite,write_json) +importFrom(jsonlite, + fromJSON, + write_json +) importFrom(methods,is) -importFrom(parsnip,augment) -importFrom(parsnip,boost_tree) -importFrom(parsnip,extract_fit_engine) -importFrom(parsnip,fit) -importFrom(parsnip,logistic_reg) -importFrom(parsnip,multinom_reg) -importFrom(parsnip,rand_forest) -importFrom(parsnip,set_engine) -importFrom(parsnip,set_mode) -importFrom(purrr,imap_dfr) -importFrom(purrr,map) -importFrom(purrr,map_int) -importFrom(purrr,pmap_chr) -importFrom(purrr,walk) +importFrom(parsnip, + augment, + boost_tree, + extract_fit_engine, + fit, + logistic_reg, + multinom_reg, + rand_forest, + set_engine, + set_mode +) +importFrom(purrr, + imap_dfr, + map, + map_int, + pmap_chr, + walk +) importFrom(readr,write_lines) -importFrom(recipes,all_predictors) -importFrom(recipes,recipe) -importFrom(recipes,step_normalize) -importFrom(recipes,step_pca) -importFrom(recipes,step_zv) -importFrom(recipes,update_role) -importFrom(rlang,":=") -importFrom(rlang,eval_tidy) -importFrom(rlang,sym) -importFrom(rsample,initial_split) -importFrom(rsample,initial_validation_split) -importFrom(rsample,testing) -importFrom(rsample,training) -importFrom(rsample,validation_set) -importFrom(rsample,vfold_cv) -importFrom(stats,coef) -importFrom(stats,fisher.test) -importFrom(stats,median) -importFrom(stats,reformulate) -importFrom(stats,reorder) -importFrom(stats,sd) -importFrom(stringr,str_remove) -importFrom(stringr,str_split) -importFrom(tibble,add_column) -importFrom(tibble,is_tibble) -importFrom(tibble,tibble) -importFrom(tidyr,drop_na) -importFrom(tidyr,pivot_longer) -importFrom(tidyr,pivot_wider) -importFrom(tune,control_grid) -importFrom(tune,extract_fit_parsnip) -importFrom(tune,finalize_workflow) -importFrom(tune,select_best) -importFrom(tune,tune_grid) -importFrom(vip,vi) -importFrom(vip,vip) -importFrom(workflows,add_model) -importFrom(workflows,add_recipe) -importFrom(workflows,workflow) -importFrom(workflowsets,extract_fit_parsnip) -importFrom(workflowsets,extract_spec_parsnip) -importFrom(yardstick,bal_accuracy) -importFrom(yardstick,conf_mat) -importFrom(yardstick,f_meas) -importFrom(yardstick,mcc) -importFrom(yardstick,metric_set) -importFrom(yardstick,pr_auc) -importFrom(yardstick,pr_curve) +importFrom(recipes, + all_predictors, + recipe, + step_normalize, + step_pca, + step_zv, + update_role +) +importFrom(rlang, + ":=", + eval_tidy, + sym +) +importFrom(rsample, + initial_split, + initial_validation_split, + testing, + training, + validation_set, + vfold_cv +) +importFrom(stats, + coef, + fisher.test, + median, + reformulate, + reorder, + sd +) +importFrom(stringr, + str_remove, + str_split +) +importFrom(tibble, + add_column, + is_tibble, + tibble +) +importFrom(tidyr, + drop_na, + pivot_longer, + pivot_wider +) +importFrom(tune, + control_grid, + extract_fit_parsnip, + finalize_workflow, + select_best, + tune_grid +) +importFrom(vip, + vi, + vip +) +importFrom(workflows, + add_model, + add_recipe, + workflow +) +importFrom(workflowsets, + extract_fit_parsnip, + extract_spec_parsnip +) +importFrom(yardstick, + bal_accuracy, + conf_mat, + f_meas, + mcc, + metric_set, + pr_auc, + pr_curve +) diff --git a/R/merge_ml_results.R b/R/merge_ml_results.R index 5da7068..9f8419a 100644 --- a/R/merge_ml_results.R +++ b/R/merge_ml_results.R @@ -12,8 +12,6 @@ #' \item{species}{Species name} #' \item{drug_label}{"drug" or "drug_class"} #' \item{drug_or_class}{Drug name or class} -#' \item{strat_label}{Stratification type (year/country) or NA} -#' \item{strat_value}{Stratification value or NA} #' \item{feature_type}{Feature category} #' \item{feature_subtype}{binary/counts} #' \item{seed}{Integer seed} @@ -132,7 +130,7 @@ parse_ml_filename <- function(filename) { #' #' @examples #' \dontrun{ -#' buildTopFeatsPq("data/Campylobacter") +#' buildTopFeatsPq("data/Campylobacter/ML_top_features/") #' } #' #' @export @@ -216,7 +214,7 @@ if (length(files) == 0) { #' #' @examples #' \dontrun{ -#' buildPerfPq("data/Campylobacter") +#' buildPerfPq("data/Campylobacter/ML_performance/") #' } #' #' @export diff --git a/man/buildPerfPq.Rd b/man/buildPerfPq.Rd index ed79c36..64139a2 100644 --- a/man/buildPerfPq.Rd +++ b/man/buildPerfPq.Rd @@ -5,31 +5,22 @@ \title{Build Parquet file of ML performance results} \usage{ buildPerfPq( - path, + perf_dir_path, out_parquet = "all_perf.parquet", compression = "zstd", verbose = TRUE ) } -\arguments{ -\item{path}{Base directory containing ML results} - -\item{out_parquet}{Output file name} - -\item{compression}{Compression method (default: "zstd")} - -\item{verbose}{Logical; print progress} -} \value{ A tibble with metadata columns + performance metrics } \description{ -Reads all \verb{_performance.tsv} files, parses metadata from filenames, +Reads all \verb{_performance.parquet} files, parses metadata from filenames, combines them into a single table, and writes a Parquet output. } \examples{ \dontrun{ -buildPerfPq("data/Campylobacter") +buildPerfPq("data/Campylobacter/ML_performance/") } } diff --git a/man/buildTopFeatsPq.Rd b/man/buildTopFeatsPq.Rd index fa3d21b..564bc88 100644 --- a/man/buildTopFeatsPq.Rd +++ b/man/buildTopFeatsPq.Rd @@ -5,14 +5,14 @@ \title{Build Parquet file of top ML features} \usage{ buildTopFeatsPq( - path, + top_feat_dir_path, out_parquet = "all_top_features.parquet", compression = "zstd", verbose = TRUE ) } \arguments{ -\item{path}{Base directory containing ML results} +\item{top_feat_dir_path}{The directory containing ML top features files} \item{out_parquet}{Output file name} @@ -24,12 +24,12 @@ buildTopFeatsPq( A tibble with metadata columns + feature importance data } \description{ -Reads all \verb{_top_features.tsv} files, parses metadata from filenames, +Reads all \verb{_top_features.parquet} files, parses metadata from filenames, combines them into a single table, and writes a Parquet file. } \examples{ \dontrun{ -buildTopFeatsPq("data/Campylobacter") +buildTopFeatsPq("data/Campylobacter/ML_top_features/") } } diff --git a/man/parse_ml_filename.Rd b/man/parse_ml_filename.Rd index 7bd48e9..fda9c8b 100644 --- a/man/parse_ml_filename.Rd +++ b/man/parse_ml_filename.Rd @@ -16,8 +16,6 @@ A named list with elements: \item{species}{Species name} \item{drug_label}{"drug" or "drug_class"} \item{drug_or_class}{Drug name or class} -\item{strat_label}{Stratification type (year/country) or NA} -\item{strat_value}{Stratification value or NA} \item{feature_type}{Feature category} \item{feature_subtype}{binary/counts} \item{seed}{Integer seed} @@ -29,7 +27,7 @@ Supports shuffled runs, drug vs drug_class, optional stratification, feature types, and seed information. } \examples{ -parse_ml_filename("Csp_drug_AMX_genes_binary_42_top_features.tsv") -parse_ml_filename("Csp_drug_year_AMX_2010-2015_genes_binary_year_42_performance.tsv") +parse_ml_filename("Csp_drug_AMX_genes_binary_42_top_features.parquet") +parse_ml_filename("Csp_drug_class_AMG_genes_binary_42_performance.parquet") } diff --git a/man/runMLmodels.Rd b/man/runMLmodels.Rd index 11001da..98c63ed 100644 --- a/man/runMLmodels.Rd +++ b/man/runMLmodels.Rd @@ -119,7 +119,7 @@ Files are saved with prefixes and suffixes indicating the configuration: \item Stratification: Suffixed with \code{"_country"} or \code{"_year"} } -For example: \code{"LOO_cross_test_ML_year_performance.tsv"} +For example: \code{"LOO_cross_test_ML_year_performance.parquet"} } \note{ This function requires the following packages: From 2b388235432c7265fa546db379e7225847cf56d2 Mon Sep 17 00:00:00 2001 From: Abhirupa Ghosh <100681585+AbhirupaGhosh@users.noreply.github.com> Date: Wed, 2 Sep 2026 11:22:42 -0600 Subject: [PATCH 11/14] Add mergeMLresults function to consolidate ML result files into parquet format --- NAMESPACE | 1 + R/merge_ml_results.R | 149 ++++++++++++++++++++++++++++++++++++++++++ man/mergeMLresults.Rd | 24 +++++++ 3 files changed, 174 insertions(+) create mode 100644 man/mergeMLresults.Rd diff --git a/NAMESPACE b/NAMESPACE index 334ddce..609584c 100644 --- a/NAMESPACE +++ b/NAMESPACE @@ -34,6 +34,7 @@ export(generateMLInputs) export(getConfusionMatrix) export(getNumFeat) export(loadMLInputTibble) +export(mergeMLresults) export(parse_ml_filename) export(plotBaselineComparison) export(plotCM) diff --git a/R/merge_ml_results.R b/R/merge_ml_results.R index 9f8419a..cf406e9 100644 --- a/R/merge_ml_results.R +++ b/R/merge_ml_results.R @@ -782,3 +782,152 @@ buildTopFeatsPqLOOYear <- function( arrow::write_parquet(merged_df, file.path(top_feat_dir_path, "LOO_year_top_features.parquet")) } + + +#' Merge all ML result files into consolidated parquet files +#' +#' Automatically detects supported ML result directories and +#' builds merged parquet summaries for each. +#' +#' @param path Base analysis directory. +#' +#' @return Invisibly returns NULL. +#' +#' @examples +#' \dontrun{ +#' mergeMLresults("data/Campylobacter") +#' } +#' +#' @export +mergeMLresults <- function(path) { + + path <- normalizePath(path) + + # ----------------------- + # Standard ML + # ----------------------- + if (dir.exists(file.path(path, "ML_performance"))) { + buildPerfPq( + perf_dir_path = file.path(path, "ML_performance") + ) + } + + if (dir.exists(file.path(path, "ML_top_features"))) { + buildTopFeatsPq( + top_feat_dir_path = file.path(path, "ML_top_features") + ) + } + + # ----------------------- + # Year stratified + # ----------------------- + if (dir.exists(file.path(path, "ML_year_performance"))) { + buildPerfPqYearCountry( + perf_dir_path = file.path(path, "ML_year_performance") + ) + } + + if (dir.exists(file.path(path, "ML_year_top_features"))) { + buildTopFeatsPqYearCountry( + top_feat_dir_path = file.path(path, "ML_year_top_features") + ) + } + + # ----------------------- + # Country stratified + # ----------------------- + if (dir.exists(file.path(path, "ML_country_performance"))) { + buildPerfPqYearCountry( + perf_dir_path = file.path(path, "ML_country_performance") + ) + } + + if (dir.exists(file.path(path, "ML_country_top_features"))) { + buildTopFeatsPqYearCountry( + top_feat_dir_path = file.path(path, "ML_country_top_features") + ) + } + + # ----------------------- + # Cross-testing + # ----------------------- + if (dir.exists(file.path(path, "cross_test_ML_performance"))) { + buildPerfPqCrossDrug( + perf_dir_path = file.path(path, "cross_test_ML_performance") + ) + } + + if (dir.exists(file.path(path, "cross_test_ML_year_performance"))) { + buildPerfPqCrossYear( + perf_dir_path = file.path(path, "cross_test_ML_year_performance") + ) + } + + if (dir.exists(file.path(path, "cross_test_ML_country_performance"))) { + buildPerfPqCrossCountry( + perf_dir_path = file.path(path, "cross_test_ML_country_performance") + ) + } + + # ----------------------- + # LOO + # ----------------------- + if (dir.exists(file.path(path, "LOO_ML_performance"))) { + buildPerfPqLOODrug( + perf_dir_path = file.path(path, "LOO_ML_performance") + ) + } + + if (dir.exists(file.path(path, "LOO_ML_top_features"))) { + buildTopFeatsPqLOODrug( + top_feat_dir_path = file.path(path, "LOO_ML_top_features") + ) + } + + if (dir.exists(file.path(path, "LOO_ML_country_performance"))) { + buildPerfPqLOOCountry( + perf_dir_path = file.path(path, "LOO_ML_country_performance") + ) + } + + if (dir.exists(file.path(path, "LOO_ML_country_top_features"))) { + buildTopFeatsPqLOOCountry( + top_feat_dir_path = file.path(path, "LOO_ML_country_top_features") + ) + } + + if (dir.exists(file.path(path, "LOO_ML_year_performance"))) { + buildPerfPqLOOYear( + perf_dir_path = file.path(path, "LOO_ML_year_performance") + ) + } + + if (dir.exists(file.path(path, "LOO_ML_year_top_features"))) { + buildTopFeatsPqLOOYear( + top_feat_dir_path = file.path(path, "LOO_ML_year_top_features") + ) + } + + # ----------------------- + # MDR + # ----------------------- + if (dir.exists(file.path(path, "MDR_ML_performance"))) { + buildPerfPqMDR( + perf_dir_path = file.path(path, "MDR_ML_performance") + ) + } + + if (dir.exists(file.path(path, "MDR_ML_top_features"))) { + buildTopFeatsPqMDR( + top_feat_dir_path = file.path(path, "MDR_ML_top_features") + ) + } + + if (dir.exists(file.path(path, "MDR_ML_pred"))) { + buildPredPqMDR( + pred_dir_path = file.path(path, "MDR_ML_pred") + ) + } + + invisible(NULL) +} \ No newline at end of file diff --git a/man/mergeMLresults.Rd b/man/mergeMLresults.Rd new file mode 100644 index 0000000..010e169 --- /dev/null +++ b/man/mergeMLresults.Rd @@ -0,0 +1,24 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/merge_ml_results.R +\name{mergeMLresults} +\alias{mergeMLresults} +\title{Merge all ML result files into consolidated parquet files} +\usage{ +mergeMLresults(path) +} +\arguments{ +\item{path}{Base analysis directory.} +} +\value{ +Invisibly returns NULL. +} +\description{ +Automatically detects supported ML result directories and +builds merged parquet summaries for each. +} +\examples{ +\dontrun{ +mergeMLresults("data/Campylobacter") +} + +} From 021a61fc584c6319aafa45cf4e0dbd236319d1ba Mon Sep 17 00:00:00 2001 From: Emily Boyer Date: Wed, 2 Sep 2026 11:21:55 -0600 Subject: [PATCH 12/14] Reject stratified filenames in parse_ml_filename() and align tests parse_ml_filename() no longer carries stratification tokens, so a year/country stratified filename was silently mislabelled (drug and seed came out wrong). Detect the strat label after drug/drug_class and stop() with a message pointing at buildPerfPqYearCountry(). Rewrite the regression tests to match the current design: .parquet fixtures, no strat_label/strat_value fields, and explicit expect_error coverage for stratified names and a bad drug token. --- R/merge_ml_results.R | 58 ++++++++++++------------- tests/testthat/test-merge-ml-results.R | 60 +++++++++++++------------- 2 files changed, 59 insertions(+), 59 deletions(-) diff --git a/R/merge_ml_results.R b/R/merge_ml_results.R index cf406e9..e6885b7 100644 --- a/R/merge_ml_results.R +++ b/R/merge_ml_results.R @@ -1,8 +1,13 @@ #' Parse ML result filename into structured metadata #' #' Extracts metadata fields encoded in machine learning result filenames. -#' Supports shuffled runs, drug vs drug_class, optional stratification, -#' feature types, and seed information. +#' Supports shuffled runs, drug vs drug_class, feature types, and seed +#' information. +#' +#' This helper handles only unstratified result filenames. Year/country +#' stratified filenames carry extra tokens and are aggregated by +#' [buildPerfPqYearCountry()] instead; passing one here raises an error +#' rather than returning silently wrong metadata. #' #' @param filename Character. Filename (not full path) to parse. #' @@ -32,8 +37,6 @@ parse_ml_filename <- function(filename) { species = NA_character_, drug_label = NA_character_, drug_or_class = NA_character_, - # strat_label = NA_character_, - # strat_value = NA_character_, feature_type = NA_character_, feature_subtype = NA_character_, seed = NA_integer_ @@ -73,43 +76,38 @@ parse_ml_filename <- function(filename) { stop("ERROR: expected 'drug' token after species") } - # # --------------------------- - # # 4. Stratified? (the strat label, if present, comes before the - # # drug/drug_class value, e.g. "..._drug_year_AMX_2010-2015_...") - # # --------------------------- - # if (i <= length(xs) && xs[i] %in% c("year", "country")) { - # out$strat_label <- xs[i] - # i <- i + 1 - # } - - # out$drug_or_class <- xs[i] - # i <- i + 1 - - # if (!is.na(out$strat_label)) { - # out$strat_value <- xs[i] - # i <- i + 1 - # } + # --------------------------- + # 4. Reject stratified filenames + # + # In stratified filenames the strat label ("year"/"country") sits right + # after "drug"/"drug_class", e.g. "..._drug_year_AMX_2010-2015_...". + # Those carry extra tokens this parser does not account for, so bail out + # with a clear message instead of silently mislabelling drug and seed. + # --------------------------- + if (i <= length(xs) && xs[i] %in% c("year", "country")) { + stop( + "parse_ml_filename() does not support the '", xs[i], + "' stratified filename '", filename, + "'; use buildPerfPqYearCountry() for stratified results." + ) + } # --------------------------- - # 4. Feature type + subtype + # 5. Drug or class value # --------------------------- out$drug_or_class <- xs[i] i <- i + 1 + + # --------------------------- + # 6. Feature type + subtype + # --------------------------- out$feature_type <- xs[i] i <- i + 1 out$feature_subtype <- xs[i] i <- i + 1 # --------------------------- - # 6. Trailing strat label (mirror) - # --------------------------- - # if (!is.na(out$strat_label)) { - # stopifnot(xs[i] == out$strat_label) - # i <- i + 1 - # } - - # --------------------------- - # 6. Seed + # 7. Seed # --------------------------- out$seed <- as.integer(xs[i]) diff --git a/tests/testthat/test-merge-ml-results.R b/tests/testthat/test-merge-ml-results.R index 58cdde8..aad362e 100644 --- a/tests/testthat/test-merge-ml-results.R +++ b/tests/testthat/test-merge-ml-results.R @@ -1,61 +1,63 @@ # Unit tests for parse_ml_filename() in merge_ml_results.R. +# +# parse_ml_filename() handles only unstratified result filenames. Year/country +# stratified filenames are aggregated by buildPerfPqYearCountry() instead. test_that("parse_ml_filename parses an unstratified drug filename", { - out <- parse_ml_filename("Csp_drug_AMX_genes_binary_42_top_features.tsv") + out <- parse_ml_filename("Csp_drug_AMX_genes_binary_42_top_features.parquet") expect_false(out$shuffled) expect_equal(out$species, "Csp") expect_equal(out$drug_label, "drug") expect_equal(out$drug_or_class, "AMX") - expect_true(is.na(out$strat_label)) - expect_true(is.na(out$strat_value)) expect_equal(out$feature_type, "genes") expect_equal(out$feature_subtype, "binary") expect_equal(out$seed, 42L) }) test_that("parse_ml_filename parses an unstratified drug_class filename", { - out <- parse_ml_filename("Csp_drug_class_AMINOGLYCOSIDES_genes_binary_42_performance.tsv") + out <- parse_ml_filename( + "Csp_drug_class_AMINOGLYCOSIDES_genes_binary_42_performance.parquet" + ) expect_equal(out$drug_label, "drug_class") expect_equal(out$drug_or_class, "AMINOGLYCOSIDES") - expect_true(is.na(out$strat_label)) expect_equal(out$seed, 42L) }) test_that("parse_ml_filename detects a shuffled run", { - out <- parse_ml_filename("shuffled_Csp_drug_AMX_genes_binary_42_top_features.tsv") + out <- parse_ml_filename( + "shuffled_Csp_drug_AMX_genes_binary_42_top_features.parquet" + ) expect_true(out$shuffled) expect_equal(out$drug_or_class, "AMX") }) -test_that("parse_ml_filename parses a year-stratified drug filename", { - # The strat label sits between "drug" and the drug value in the actual - # filenames written by the matrix-generation code, e.g. - # "_drug_year___...". - out <- parse_ml_filename( - "Csp_drug_year_AMX_2010-2015_genes_binary_year_42_performance.tsv" +test_that("parse_ml_filename errors on a non-'drug' token after species", { + expect_error( + parse_ml_filename("Csp_widget_AMX_genes_binary_42_performance.parquet"), + "expected 'drug' token" ) - - expect_equal(out$species, "Csp") - expect_equal(out$drug_label, "drug") - expect_equal(out$drug_or_class, "AMX") - expect_equal(out$strat_label, "year") - expect_equal(out$strat_value, "2010-2015") - expect_equal(out$feature_type, "genes") - expect_equal(out$feature_subtype, "binary") - expect_equal(out$seed, 42L) }) -test_that("parse_ml_filename parses a country-stratified drug_class filename", { - out <- parse_ml_filename( - "Csp_drug_class_country_AMINOGLYCOSIDES_USA_genes_binary_country_7_top_features.tsv" +test_that("parse_ml_filename rejects a year-stratified filename", { + # The strat label sits between "drug" and the drug value in stratified + # filenames, e.g. "_drug_year___...". These are + # handled by buildPerfPqYearCountry(), not this parser. + expect_error( + parse_ml_filename( + "Csp_drug_year_AMX_2010-2015_genes_binary_year_42_performance.parquet" + ), + "does not support the 'year' stratified filename" ) +}) - expect_equal(out$drug_label, "drug_class") - expect_equal(out$drug_or_class, "AMINOGLYCOSIDES") - expect_equal(out$strat_label, "country") - expect_equal(out$strat_value, "USA") - expect_equal(out$seed, 7L) +test_that("parse_ml_filename rejects a country-stratified filename", { + expect_error( + parse_ml_filename( + "Csp_drug_class_country_AMINOGLYCOSIDES_USA_genes_binary_country_7_top_features.parquet" + ), + "does not support the 'country' stratified filename" + ) }) From a72bc862e877214875c5b34da347df0e9bdd48ff Mon Sep 17 00:00:00 2001 From: Emily Boyer Date: Wed, 2 Sep 2026 11:24:12 -0600 Subject: [PATCH 13/14] Document buildPerfPq() parameters The path argument was renamed to perf_dir_path without updating the roxygen block, leaving all four arguments undocumented (R CMD check WARNING). --- R/merge_ml_results.R | 4 ++++ man/buildPerfPq.Rd | 9 +++++++++ 2 files changed, 13 insertions(+) diff --git a/R/merge_ml_results.R b/R/merge_ml_results.R index e6885b7..c2d5bd3 100644 --- a/R/merge_ml_results.R +++ b/R/merge_ml_results.R @@ -207,6 +207,10 @@ if (length(files) == 0) { #' Reads all `_performance.parquet` files, parses metadata from filenames, #' combines them into a single table, and writes a Parquet output. #' +#' @param perf_dir_path The directory containing ML performance files +#' @param out_parquet Output file name +#' @param compression Compression method (default: "zstd") +#' @param verbose Logical; print progress #' #' @return A tibble with metadata columns + performance metrics #' diff --git a/man/buildPerfPq.Rd b/man/buildPerfPq.Rd index 64139a2..18cf670 100644 --- a/man/buildPerfPq.Rd +++ b/man/buildPerfPq.Rd @@ -11,6 +11,15 @@ buildPerfPq( verbose = TRUE ) } +\arguments{ +\item{perf_dir_path}{The directory containing ML performance files} + +\item{out_parquet}{Output file name} + +\item{compression}{Compression method (default: "zstd")} + +\item{verbose}{Logical; print progress} +} \value{ A tibble with metadata columns + performance metrics } From c3753dd5c0f6706cca6e9291e7dc2fe33c271e76 Mon Sep 17 00:00:00 2001 From: Emily Boyer Date: Wed, 2 Sep 2026 12:46:55 -0600 Subject: [PATCH 14/14] Add tests for buildPerfPqYearCountry() and buildTopFeatsPqYearCountry() Cover filename parsing for the single-token stratified format: year and country stratification, drug and drug_class, plus the Importance numeric coercion in the top-features aggregator. --- tests/testthat/test-merge-ml-results.R | 67 ++++++++++++++++++++++++++ 1 file changed, 67 insertions(+) diff --git a/tests/testthat/test-merge-ml-results.R b/tests/testthat/test-merge-ml-results.R index aad362e..f367698 100644 --- a/tests/testthat/test-merge-ml-results.R +++ b/tests/testthat/test-merge-ml-results.R @@ -61,3 +61,70 @@ test_that("parse_ml_filename rejects a country-stratified filename", { "does not support the 'country' stratified filename" ) }) + +# --------------------------------------------------------------------------- +# buildPerfPqYearCountry() / buildTopFeatsPqYearCountry() filename parsing. +# Stratified filenames carry the strat label once, in the prefix: +# ________... +# --------------------------------------------------------------------------- + +test_that("buildPerfPqYearCountry parses year-stratified drug filenames", { + dir <- withr::local_tempdir() + arrow::write_parquet( + tibble::tibble(metric = "roc_auc", estimate = 0.9), + file.path(dir, "Csp_drug_year_AMX_2010-2015_genes_binary_42_performance.parquet") + ) + arrow::write_parquet( + tibble::tibble(metric = "roc_auc", estimate = 0.8), + file.path(dir, "Csp_drug_year_CIP_2016-2020_genes_binary_42_performance.parquet") + ) + + out <- buildPerfPqYearCountry(dir) + + expect_setequal(out$drug_or_class, c("AMX", "CIP")) + expect_setequal(out$strat_value, c("2010-2015", "2016-2020")) + expect_true(all(out$species == "Csp")) + expect_true(all(out$drug_label == "drug")) + expect_true(all(out$strat_label == "year")) + expect_true(all(out$feature_type == "genes")) + expect_true(all(out$feature_subtype == "binary")) + expect_false("seed_from_name" %in% names(out)) + expect_true(file.exists(file.path(dir, "year_perf.parquet"))) +}) + +test_that("buildPerfPqYearCountry parses country-stratified drug_class filenames", { + dir <- withr::local_tempdir() + arrow::write_parquet( + tibble::tibble(metric = "roc_auc", estimate = 0.7), + file.path( + dir, + "Csp_drug_class_country_AMINOGLYCOSIDES_USA_genes_binary_7_performance.parquet" + ) + ) + + out <- buildPerfPqYearCountry(dir) + + expect_equal(out$drug_label, "drug_class") + expect_equal(out$strat_label, "country") + expect_equal(out$drug_or_class, "AMINOGLYCOSIDES") + expect_equal(out$strat_value, "USA") + expect_true(file.exists(file.path(dir, "country_perf.parquet"))) +}) + +test_that("buildTopFeatsPqYearCountry parses stratified filenames", { + dir <- withr::local_tempdir() + arrow::write_parquet( + tibble::tibble(Feature = "gyrA", Importance = "0.42"), + file.path(dir, "Csp_drug_year_AMX_2010-2015_genes_binary_42_top_features.parquet") + ) + + buildTopFeatsPqYearCountry(dir) + + res <- arrow::read_parquet(file.path(dir, "year_top_features.parquet")) + expect_equal(res$species, "Csp") + expect_equal(res$drug_label, "drug") + expect_equal(res$strat_label, "year") + expect_equal(res$drug_or_class, "AMX") + expect_equal(res$strat_value, "2010-2015") + expect_type(res$Importance, "double") +})