From 5a7d650edabda316fb90c6c8bfc4de314321162b Mon Sep 17 00:00:00 2001 From: Cvicnaire Date: Thu, 15 Oct 2026 08:22:19 -0400 Subject: [PATCH] mergeDifexp(): return the matrix with a signature metadata table mergeDifexp() now returns list(matrix, metadata) instead of a bare matrix. `metadata` is a data frame with one row per matrix column, in column order and with the column names as row names: signature_name, the database signature_id (NA for supplied objects), every scalar field of each signature's metadata (vector fields collapsed with "; ", nested lists such as `others` left out) and n_features, the non-NA count of that column. It plays the role of a SummarizedExperiment's colData while staying plain base R so it survives export outside R. To know which database id each fetched signature came from, fetchCompareSignatures() now also returns `ids`, and resolveCompareSignatureList() gains `with_ids = TRUE` to pass them on. compareSignatures() is unchanged. Closes #282 Co-Authored-By: Claude Fable 5.1 --- NEWS.Rmd | 16 ++++ NEWS.md | 16 ++++ R/compareSignature.R | 23 +++++- R/mergeDifexp.R | 116 ++++++++++++++++++++++++++--- man/mergeDifexp.Rd | 38 ++++++++-- tests/testthat/test_mergeDifexp.R | 120 ++++++++++++++++++++++++++---- 6 files changed, 292 insertions(+), 37 deletions(-) diff --git a/NEWS.Rmd b/NEWS.Rmd index 35e155e..e516c60 100644 --- a/NEWS.Rmd +++ b/NEWS.Rmd @@ -3,6 +3,22 @@ output: rmarkdown::github_document --- +# SigRepo (development version) + +## Breaking changes + +- `mergeDifexp()` returns a list of `matrix` and `metadata` instead of a + bare matrix (#282). `matrix` is the features x signatures matrix as + before; `metadata` is a data frame with one row per matrix column, in + column order, holding `signature_name`, the database `signature_id` (`NA` + for objects supplied directly), every scalar field of the signatures' + metadata (`organism`, `phenotype`, `platform`, `PMID`, cutoffs, ...; vector + fields such as `keywords` collapsed with `"; "`, nested lists such as + `others` left out) and `n_features`, the number of non-`NA` cells in that + column. The pair plays the role of a SummarizedExperiment's assay and + `colData` while staying plain base R objects. Code that used the result as + a matrix needs `$matrix`. + # SigRepo 1.1.0 ## Breaking changes diff --git a/NEWS.md b/NEWS.md index e6b6602..9f1ad10 100755 --- a/NEWS.md +++ b/NEWS.md @@ -1,3 +1,19 @@ +# SigRepo (development version) + +## Breaking changes + +- `mergeDifexp()` returns a list of `matrix` and `metadata` instead of a + bare matrix (#282). `matrix` is the features x signatures matrix as + before; `metadata` is a data frame with one row per matrix column, in + column order, holding `signature_name`, the database `signature_id` + (`NA` for objects supplied directly), every scalar field of the + signatures’ metadata (`organism`, `phenotype`, `platform`, `PMID`, + cutoffs, …; vector fields such as `keywords` collapsed with `"; "`, + nested lists such as `others` left out) and `n_features`, the number + of non-`NA` cells in that column. The pair plays the role of a + SummarizedExperiment’s assay and `colData` while staying plain base R + objects. Code that used the result as a matrix needs `$matrix`. + # SigRepo 1.1.0 ## Breaking changes diff --git a/R/compareSignature.R b/R/compareSignature.R index d2ffb92..9846fd8 100644 --- a/R/compareSignature.R +++ b/R/compareSignature.R @@ -424,12 +424,18 @@ fillRankingPValues <- function(sig_list, p_value_col, adj_p_col) { #' @param omic_signatures Supplied objects, list, collection, or NULL. #' @param arg_suffix "" for the first list, "2" for the second; used in messages. #' @param verbose Passed on to the database helpers. -#' @return A named list of OmicSignature objects, possibly empty. +#' @param with_ids When TRUE, return a list with `signatures` (the named list) +#' and `ids` (the database id of each one as a character vector, NA for +#' supplied objects) instead of the bare list. +#' @return A named list of OmicSignature objects, possibly empty; or, with +#' `with_ids = TRUE`, a list holding that list as `signatures` and their +#' database ids as `ids`. #' @noRd resolveCompareSignatureList <- function(conn_handler, signature_ids, signature_names, omic_signatures, - arg_suffix = "", verbose = FALSE) { + arg_suffix = "", verbose = FALSE, with_ids = FALSE) { fetched <- base::list() + fetched_ids <- base::character() problems <- base::character() if (base::length(c(signature_ids, signature_names)) > 0) { got <- fetchCompareSignatures( @@ -440,6 +446,7 @@ resolveCompareSignatureList <- function(conn_handler, signature_ids, signature_n verbose = verbose ) fetched <- got$signatures + fetched_ids <- got$ids problems <- got$problems } @@ -463,6 +470,13 @@ resolveCompareSignatureList <- function(conn_handler, signature_ids, signature_n ) } + if (with_ids) { + # Supplied objects have no database id. + return(base::list( + signatures = signatures, + ids = c(fetched_ids, base::rep(NA_character_, base::length(supplied))) + )) + } signatures } @@ -470,7 +484,8 @@ resolveCompareSignatureList <- function(conn_handler, signature_ids, signature_n #' Fetch requested signatures from the database and report what could not be #' #' @return A list with `signatures` (a named list of OmicSignature objects, -#' possibly empty) and `problems` (a character vector, one entry per id or +#' possibly empty), `ids` (their database ids as a character vector, in the +#' same order) and `problems` (a character vector, one entry per id or #' name that could not be honoured). #' @noRd fetchCompareSignatures <- function(conn_handler, signature_ids, signature_names, arg_suffix = "", verbose = FALSE) { @@ -542,7 +557,7 @@ fetchCompareSignatures <- function(conn_handler, signature_ids, signature_names, SigRepo::verbose(base::sprintf("Retrieved %d signature(s) from the database.\n", base::length(fetched))) - base::list(signatures = fetched, problems = problems) + base::list(signatures = fetched, ids = fetched_ids, problems = problems) } diff --git a/R/mergeDifexp.R b/R/mergeDifexp.R index 119ab00..f8b70a9 100644 --- a/R/mergeDifexp.R +++ b/R/mergeDifexp.R @@ -7,6 +7,12 @@ #' value in each cell is that signature's \code{value_col} for that feature, #' and \code{NA} where the signature's difexp table has no such feature. #' +#' The matrix comes with a metadata table describing its columns: one row per +#' signature, in column order, built from each signature's metadata. The pair +#' plays the role of the assay and \code{colData} of a SummarizedExperiment, +#' but both are plain base R objects (a matrix and a data frame) so they can +#' be written out and used outside R without any Bioconductor dependency. +#' #' This is the "signature compendium" shape that signature-by-signature #' correlation, clustering, PCA and reference databases for tools such as #' signatureSearch expect. The thresholded signature tables are not used; @@ -49,8 +55,24 @@ #' @param verbose Logical; whether to print diagnostic messages while #' resolving and fetching signatures. Defaults to \code{FALSE}. #' -#' @return A numeric matrix with features as row names and signatures as -#' column names, in the order the signatures were assembled. +#' @return A list with two elements: +#' \describe{ +#' \item{\code{matrix}}{A numeric matrix with features as row names and +#' signatures as column names, in the order the signatures were +#' assembled.} +#' \item{\code{metadata}}{A data frame with one row per column of +#' \code{matrix}, in the same order, whose row names are the column +#' names of \code{matrix}. It starts with \code{signature_name} and +#' \code{signature_id} (the database id, \code{NA} for objects supplied +#' directly), followed by one column for every scalar field found in +#' any signature's metadata (\code{organism}, \code{phenotype}, +#' \code{platform}, \code{PMID}, cutoffs, ...), \code{NA} where a +#' signature lacks the field. Metadata fields holding several values, +#' such as \code{keywords}, are collapsed into one string separated by +#' \code{"; "}; nested list fields such as \code{others} are left out. +#' The last column, \code{n_features}, counts the non-\code{NA} cells +#' of that signature's column.} +#' } #' #' @seealso \code{compareSignatures()}, \code{getSignature()}. #' @@ -58,11 +80,12 @@ #' # The bundled example signatures need no database #' utils::data("omic_signature_1", "omic_signature_2", "omic_signature_3", package = "SigRepo") #' -#' m <- SigRepo::mergeDifexp( +#' merged <- SigRepo::mergeDifexp( #' omic_signatures = list(v1 = omic_signature_1, v2 = omic_signature_2, v3 = omic_signature_3) #' ) -#' dim(m) -#' stats::cor(m, use = "pairwise.complete.obs") +#' dim(merged$matrix) +#' merged$metadata[, c("signature_name", "organism", "phenotype", "n_features")] +#' stats::cor(merged$matrix, use = "pairwise.complete.obs") #' #' \dontrun{ #' conn_handler <- SigRepo::newConnHandler( @@ -70,13 +93,14 @@ #' user = "your_username", password = "your_password" #' ) #' -#' m <- SigRepo::mergeDifexp( +#' merged <- SigRepo::mergeDifexp( #' conn_handler = conn_handler, #' signature_ids = c(12, 34), #' signature_names = "my_signature", #' value_col = "logfc", #' features = "intersect" #' ) +#' merged$metadata$signature_id #' #' # Adjusted p-values: keep the most significant probe of a duplicated feature #' p <- SigRepo::mergeDifexp( @@ -84,7 +108,7 @@ #' signature_ids = c(12, 34), #' value_col = "adj_p", #' collapse = "min" -#' ) +#' )$matrix #' } #' #' @export @@ -113,20 +137,24 @@ mergeDifexp <- function( base::stop("\n'conn_handler' is required to fetch signatures by 'signature_ids' or 'signature_names'.\n") } - sig_list <- resolveCompareSignatureList( + resolved <- resolveCompareSignatureList( conn_handler = conn_handler, signature_ids = signature_ids, signature_names = signature_names, omic_signatures = omic_signatures, arg_suffix = "", - verbose = verbose + verbose = verbose, + with_ids = TRUE ) + sig_list <- resolved$signatures if (base::length(sig_list) == 0) { base::stop("\nProvide signatures through 'signature_ids', 'signature_names' and/or 'omic_signatures'.\n") } - # One named numeric vector per usable signature: feature -> value. + # One named numeric vector per usable signature: feature -> value. `kept` + # remembers which signatures made it, so their metadata can be lined up. columns <- base::list() + kept <- base::integer() dropped <- base::character() for (i in base::seq_along(sig_list)) { sig_name <- base::names(sig_list)[i] @@ -143,11 +171,13 @@ mergeDifexp <- function( )) next } - columns[[sig_name]] <- collapseDifexpValues( + columns[[base::length(columns) + 1L]] <- collapseDifexpValues( feature = base::as.character(difexp[[feature_col]]), value = base::as.numeric(difexp[[value_col]]), collapse = collapse ) + base::names(columns)[base::length(columns)] <- sig_name + kept <- c(kept, i) } if (base::length(dropped) > 0) { @@ -182,7 +212,69 @@ mergeDifexp <- function( nrow = base::length(all_features), dimnames = base::list(all_features, base::names(columns)) ) - merged + + metadata <- difexpSignatureMetadata( + signatures = sig_list[kept], + signature_ids = resolved$ids[kept], + n_features = base::colSums(!base::is.na(merged)) + ) + base::rownames(metadata) <- base::make.unique(base::colnames(merged)) + + base::list(matrix = merged, metadata = metadata) +} + + +#' Build the signature-level metadata table of a merged matrix +#' +#' Flattens each signature's metadata list into one row: atomic fields of +#' length one are kept as they are, longer atomic vectors are pasted with +#' "; ", empty fields become NA and anything else (nested lists such as +#' `others`) is left out. A column's type is numeric or logical when every +#' present value is, and character otherwise. +#' +#' @param signatures List of OmicSignature objects, one per matrix column. +#' @param signature_ids Character vector of database ids, NA where unknown. +#' @param n_features Integer vector of non-NA cells per matrix column. +#' @return A data frame with one row per signature, columns +#' `signature_name`, `signature_id`, the flattened metadata fields in order +#' of first appearance, and `n_features`. +#' @noRd +difexpSignatureMetadata <- function(signatures, signature_ids, n_features) { + flat <- base::lapply(signatures, function(sig) { + md <- sig$metadata + if (!base::is.list(md)) { + return(base::list()) + } + md <- md[base::vapply(md, function(v) base::is.atomic(v) && !base::is.null(v), base::logical(1))] + base::lapply(md, function(v) { + if (base::length(v) == 0) NA else if (base::length(v) == 1) v else base::paste(v, collapse = "; ") + }) + }) + + fields <- base::unique(base::unlist(base::lapply(flat, base::names), use.names = FALSE)) + fields <- c("signature_name", base::setdiff(fields, c("signature_name", "signature_id"))) + + columns <- base::lapply(fields, function(field) { + values <- base::lapply(flat, function(row) if (field %in% base::names(row)) row[[field]] else NA) + present <- values[!base::vapply(values, function(v) base::is.na(v), base::logical(1))] + if (base::all(base::vapply(present, base::is.numeric, base::logical(1)))) { + base::vapply(values, function(v) if (base::is.na(v)) NA_real_ else base::as.numeric(v), base::numeric(1)) + } else if (base::all(base::vapply(present, base::is.logical, base::logical(1)))) { + base::vapply(values, function(v) if (base::is.na(v)) NA else base::as.logical(v), base::logical(1)) + } else { + base::vapply(values, function(v) if (base::is.na(v)) NA_character_ else base::as.character(v), base::character(1)) + } + }) + base::names(columns) <- fields + + metadata <- base::data.frame(columns, stringsAsFactors = FALSE, check.names = FALSE) + base::cbind( + metadata[, "signature_name", drop = FALSE], + signature_id = base::suppressWarnings(base::as.integer(signature_ids)), + metadata[, base::setdiff(fields, "signature_name"), drop = FALSE], + n_features = base::as.integer(n_features), + stringsAsFactors = FALSE + ) } diff --git a/man/mergeDifexp.Rd b/man/mergeDifexp.Rd index a9db3c3..cae5848 100644 --- a/man/mergeDifexp.Rd +++ b/man/mergeDifexp.Rd @@ -49,8 +49,24 @@ or \code{"adj_p"}, where the smallest value is the most significant; resolving and fetching signatures. Defaults to \code{FALSE}.} } \value{ -A numeric matrix with features as row names and signatures as - column names, in the order the signatures were assembled. +A list with two elements: + \describe{ + \item{\code{matrix}}{A numeric matrix with features as row names and + signatures as column names, in the order the signatures were + assembled.} + \item{\code{metadata}}{A data frame with one row per column of + \code{matrix}, in the same order, whose row names are the column + names of \code{matrix}. It starts with \code{signature_name} and + \code{signature_id} (the database id, \code{NA} for objects supplied + directly), followed by one column for every scalar field found in + any signature's metadata (\code{organism}, \code{phenotype}, + \code{platform}, \code{PMID}, cutoffs, ...), \code{NA} where a + signature lacks the field. Metadata fields holding several values, + such as \code{keywords}, are collapsed into one string separated by + \code{"; "}; nested list fields such as \code{others} are left out. + The last column, \code{n_features}, counts the non-\code{NA} cells + of that signature's column.} + } } \description{ Fetches signatures from the SigRepo database and/or takes OmicSignature @@ -59,6 +75,12 @@ numeric matrix with one row per feature and one column per signature. The value in each cell is that signature's \code{value_col} for that feature, and \code{NA} where the signature's difexp table has no such feature. +The matrix comes with a metadata table describing its columns: one row per +signature, in column order, built from each signature's metadata. The pair +plays the role of the assay and \code{colData} of a SummarizedExperiment, +but both are plain base R objects (a matrix and a data frame) so they can +be written out and used outside R without any Bioconductor dependency. + This is the "signature compendium" shape that signature-by-signature correlation, clustering, PCA and reference databases for tools such as signatureSearch expect. The thresholded signature tables are not used; @@ -82,11 +104,12 @@ warning naming them; it is an error when that leaves nothing to merge. # The bundled example signatures need no database utils::data("omic_signature_1", "omic_signature_2", "omic_signature_3", package = "SigRepo") -m <- SigRepo::mergeDifexp( +merged <- SigRepo::mergeDifexp( omic_signatures = list(v1 = omic_signature_1, v2 = omic_signature_2, v3 = omic_signature_3) ) -dim(m) -stats::cor(m, use = "pairwise.complete.obs") +dim(merged$matrix) +merged$metadata[, c("signature_name", "organism", "phenotype", "n_features")] +stats::cor(merged$matrix, use = "pairwise.complete.obs") \dontrun{ conn_handler <- SigRepo::newConnHandler( @@ -94,13 +117,14 @@ conn_handler <- SigRepo::newConnHandler( user = "your_username", password = "your_password" ) -m <- SigRepo::mergeDifexp( +merged <- SigRepo::mergeDifexp( conn_handler = conn_handler, signature_ids = c(12, 34), signature_names = "my_signature", value_col = "logfc", features = "intersect" ) +merged$metadata$signature_id # Adjusted p-values: keep the most significant probe of a duplicated feature p <- SigRepo::mergeDifexp( @@ -108,7 +132,7 @@ p <- SigRepo::mergeDifexp( signature_ids = c(12, 34), value_col = "adj_p", collapse = "min" -) +)$matrix } } diff --git a/tests/testthat/test_mergeDifexp.R b/tests/testthat/test_mergeDifexp.R index be0f6e6..d5a8390 100644 --- a/tests/testthat/test_mergeDifexp.R +++ b/tests/testthat/test_mergeDifexp.R @@ -72,7 +72,7 @@ mock_conn_handler <- base::list(dbname = "sigrepo", host = "mock", port = 3306, test_that("the result is a feature-by-signature matrix with the union of features", { sigs <- toy_pair() - m <- SigRepo::mergeDifexp(omic_signatures = sigs) + m <- SigRepo::mergeDifexp(omic_signatures = sigs)$matrix expect_true(base::is.matrix(m)) expect_type(m, "double") @@ -87,7 +87,7 @@ test_that("the result is a feature-by-signature matrix with the union of feature test_that("unnamed objects are named by their signature_name", { sigs <- base::unname(toy_pair()) - m <- SigRepo::mergeDifexp(omic_signatures = sigs) + m <- SigRepo::mergeDifexp(omic_signatures = sigs)$matrix expect_equal(base::colnames(m), c("sigA", "sigB")) }) @@ -95,7 +95,7 @@ test_that("unnamed objects are named by their signature_name", { test_that("features = 'intersect' keeps only features present in every table", { sigs <- toy_pair() - m <- SigRepo::mergeDifexp(omic_signatures = sigs, features = "intersect") + m <- SigRepo::mergeDifexp(omic_signatures = sigs, features = "intersect")$matrix expect_setequal(base::rownames(m), c("G1", "G3")) expect_false(base::anyNA(m)) @@ -106,7 +106,7 @@ test_that("features = 'intersect' keeps only features present in every table", { test_that("duplicate feature names collapse to the value with the largest absolute size by default", { sigs <- toy_pair() - m <- SigRepo::mergeDifexp(omic_signatures = sigs) + m <- SigRepo::mergeDifexp(omic_signatures = sigs)$matrix # G2 appears twice in sigA with scores -2 and 5. expect_equal(m["G2", "a"], 5) @@ -116,7 +116,7 @@ test_that("duplicate feature names collapse to the value with the largest absolu test_that("collapse = 'mean' averages duplicate feature names", { sigs <- toy_pair() - m <- SigRepo::mergeDifexp(omic_signatures = sigs, collapse = "mean") + m <- SigRepo::mergeDifexp(omic_signatures = sigs, collapse = "mean")$matrix expect_equal(m["G2", "a"], 1.5) }) @@ -124,7 +124,7 @@ test_that("collapse = 'mean' averages duplicate feature names", { test_that("collapse = 'first' keeps the first row of a duplicated feature", { sigs <- toy_pair() - m <- SigRepo::mergeDifexp(omic_signatures = sigs, collapse = "first") + m <- SigRepo::mergeDifexp(omic_signatures = sigs, collapse = "first")$matrix expect_equal(m["G2", "a"], -2) }) @@ -132,7 +132,7 @@ test_that("collapse = 'first' keeps the first row of a duplicated feature", { test_that("collapse = 'min' keeps the smallest value of a duplicated feature", { sigs <- toy_pair() - m <- SigRepo::mergeDifexp(omic_signatures = sigs, collapse = "min") + m <- SigRepo::mergeDifexp(omic_signatures = sigs, collapse = "min")$matrix expect_equal(m["G2", "a"], -2) expect_equal(m["G1", "a"], 1) @@ -144,7 +144,7 @@ test_that("collapse = 'min' on a p-value column keeps the most significant probe p_value = c(0.5, 0.2, 0.001) )) - m <- SigRepo::mergeDifexp(omic_signatures = base::list(a = a), value_col = "p_value", collapse = "min") + m <- SigRepo::mergeDifexp(omic_signatures = base::list(a = a), value_col = "p_value", collapse = "min")$matrix expect_equal(m["G2", "a"], 0.001) expect_equal(m["G1", "a"], 0.5) @@ -157,7 +157,7 @@ test_that("collapse = 'min' ignores NA values within a feature and returns NA wh p_value = c(NA, 0.3, NA, NA) )) - m <- SigRepo::mergeDifexp(omic_signatures = base::list(a = a), value_col = "p_value", collapse = "min") + m <- SigRepo::mergeDifexp(omic_signatures = base::list(a = a), value_col = "p_value", collapse = "min")$matrix expect_equal(m["G1", "a"], 0.3) expect_true(base::is.na(m["G2", "a"])) @@ -168,7 +168,7 @@ test_that("collapse = 'min' ignores NA values within a feature and returns NA wh test_that("value_col picks another difexp column", { sigs <- toy_pair() - m <- SigRepo::mergeDifexp(omic_signatures = sigs, value_col = "logfc") + m <- SigRepo::mergeDifexp(omic_signatures = sigs, value_col = "logfc")$matrix expect_equal(m["G1", ], c(a = 0.5, b = -0.5)) }) @@ -180,7 +180,7 @@ test_that("a signature whose difexp lacks value_col is dropped with a warning", sigs$b$difexp <- difexp expect_warning( - m <- SigRepo::mergeDifexp(omic_signatures = sigs, value_col = "logfc"), + m <- SigRepo::mergeDifexp(omic_signatures = sigs, value_col = "logfc")$matrix, "b" ) expect_equal(base::colnames(m), "a") @@ -193,7 +193,7 @@ test_that("a signature without a difexp table is dropped with a warning", { no_difexp <- OmicSignature::OmicSignature$new(metadata = sigs$b$metadata, signature = sigs$b$signature) sigs$b <- no_difexp - expect_warning(m <- SigRepo::mergeDifexp(omic_signatures = sigs), "b") + expect_warning(m <- SigRepo::mergeDifexp(omic_signatures = sigs)$matrix, "b") expect_equal(base::colnames(m), "a") }) @@ -216,7 +216,7 @@ test_that("it is an error when no signatures are given at all", { test_that("the bundled signatures merge into one matrix with every feature once", { sigs <- example_signatures() - m <- SigRepo::mergeDifexp(omic_signatures = sigs) + m <- SigRepo::mergeDifexp(omic_signatures = sigs)$matrix expect_equal(base::colnames(m), c("v1", "v2", "v3")) expect_setequal(base::rownames(m), base::unique(sigs$v1$difexp$feature_name)) @@ -234,7 +234,7 @@ test_that("signatures requested by id and name are fetched through getSignature" db <- mock_database(sigs, rows) testthat::local_mocked_bindings(searchSignature = db$search, getSignature = db$get, .package = "SigRepo") - m <- SigRepo::mergeDifexp(conn_handler = mock_conn_handler, signature_ids = 21, signature_names = "sigB") + m <- SigRepo::mergeDifexp(conn_handler = mock_conn_handler, signature_ids = 21, signature_names = "sigB")$matrix expect_equal(base::colnames(m), c("sigA", "sigB")) expect_equal(base::length(db$calls$get), 2L) @@ -244,3 +244,95 @@ test_that("signatures requested by id and name are fetched through getSignature" test_that("a connection is required to fetch by id", { expect_error(SigRepo::mergeDifexp(signature_ids = 1), "conn_handler") }) + +## Metadata #### + +test_that("the result is a list holding the matrix and a metadata table", { + sigs <- toy_pair() + + res <- SigRepo::mergeDifexp(omic_signatures = sigs) + + expect_type(res, "list") + expect_named(res, c("matrix", "metadata")) + expect_true(base::is.matrix(res$matrix)) + expect_s3_class(res$metadata, "data.frame") +}) + +test_that("metadata has one row per matrix column, in column order", { + sigs <- toy_pair() + + res <- SigRepo::mergeDifexp(omic_signatures = sigs) + + expect_equal(base::nrow(res$metadata), base::ncol(res$matrix)) + expect_equal(base::rownames(res$metadata), base::colnames(res$matrix)) + expect_equal(res$metadata$signature_name, c("sigA", "sigB")) + expect_equal(base::colnames(res$metadata)[1:2], c("signature_name", "signature_id")) + expect_equal(utils::tail(base::colnames(res$metadata), 1), "n_features") +}) + +test_that("scalar metadata fields become columns, vectors are collapsed and nested lists dropped", { + sigs <- toy_pair() + + md <- SigRepo::mergeDifexp(omic_signatures = sigs)$metadata + + expect_equal(md$organism, c("Mus musculus", "Mus musculus")) + expect_equal(md$year, c(2015, 2015)) + expect_type(md$year, "double") + expect_false("others" %in% base::colnames(md)) + + # The bundled signatures keep keywords as a character vector. + bundled <- SigRepo::mergeDifexp(omic_signatures = example_signatures())$metadata + expect_equal(bundled$keywords, base::rep("Myc; KO; longevity", 3)) +}) + +test_that("a field missing from one signature is NA for that signature", { + sigs <- toy_pair() + md_a <- sigs$a$metadata + md_a$PMID <- NULL + md_a$batch <- "b1" + sigs$a$metadata <- md_a + + md <- SigRepo::mergeDifexp(omic_signatures = sigs)$metadata + + expect_true(base::is.na(md["a", "PMID"])) + expect_equal(md["b", "PMID"], "25619689") + expect_equal(md$batch, c("b1", NA)) +}) + +test_that("n_features counts the non-missing cells of each signature's column", { + sigs <- toy_pair() + + union <- SigRepo::mergeDifexp(omic_signatures = sigs) + intersect <- SigRepo::mergeDifexp(omic_signatures = sigs, features = "intersect") + + expect_equal(union$metadata$n_features, c(3L, 3L)) + expect_equal(intersect$metadata$n_features, c(2L, 2L)) +}) + +test_that("dropped signatures do not appear in the metadata", { + sigs <- toy_pair() + sigs$b <- OmicSignature::OmicSignature$new(metadata = sigs$b$metadata, signature = sigs$b$signature) + + expect_warning(res <- SigRepo::mergeDifexp(omic_signatures = sigs), "b") + + expect_equal(base::rownames(res$metadata), "a") + expect_equal(base::nrow(res$metadata), 1L) +}) + +test_that("signature_id is filled for fetched signatures and NA for supplied ones", { + sigs <- toy_pair() + rows <- base::data.frame( + signature_id = c(21L, 22L), signature_name = c("sigA", "sigB"), user_name = "tester", + key = c("a", "b"), stringsAsFactors = FALSE + ) + db <- mock_database(sigs, rows) + testthat::local_mocked_bindings(searchSignature = db$search, getSignature = db$get, .package = "SigRepo") + + res <- SigRepo::mergeDifexp( + conn_handler = mock_conn_handler, signature_ids = 21, omic_signatures = base::list(local = sigs$b) + ) + + expect_equal(base::colnames(res$matrix), c("sigA", "local")) + expect_equal(res$metadata$signature_id, c(21L, NA)) + expect_equal(res$metadata$signature_name, c("sigA", "sigB")) +})