diff --git a/DESCRIPTION b/DESCRIPTION index 7670c5f..224a071 100755 --- a/DESCRIPTION +++ b/DESCRIPTION @@ -10,7 +10,7 @@ Description: BreastSubtypeR provides an assumption-aware, multi-method framework (iBreastSubtypeR) is included for interactive analyses and to support users without programming experience. Encoding: UTF-8 -Version: 1.3.3 +Version: 1.5.1 biocViews: RNASeq, Software, GeneExpression, Classification, Preprocessing, Visualization Authors@R: c( person(given = "Qiao", family = "Yang", @@ -59,4 +59,4 @@ License: GPL-3 VignetteBuilder: knitr Roxygen: list(markdown = TRUE, roclets = c("rd", "namespace", "collate")) LazyData: FALSE -RoxygenNote: 7.3.3 +RoxygenNote: 8.0.0 diff --git a/NEWS.md b/NEWS.md index 9f9df9e..37519eb 100755 --- a/NEWS.md +++ b/NEWS.md @@ -1,65 +1,73 @@ -# BreastSubtypeR 1.3.2 +# BreastSubtypeR 1.5.1 + +## Bug fixes + +- Fixed TNBC cohort detection in cohort-specific method selection (#133). Cohorts are now classified as TNBC only when all evaluable non-missing `TN` annotations indicate TN. Mixed cohorts containing both TN and nonTN samples are no longer routed to the TNBC-specific branch solely because some samples are TN. + +## Tests + +- Added synthetic phenotype-table tests for TN-only, mixed, UNC-like mixed, and missing or partly missing TN annotations. + +# BreastSubtypeR 1.5.0 ## Highlights (from v1.1.3 onward) -- Paper published in *NAR Genomics and Bioinformatics* (2025), **Editor’s Choice** (DOI: 10.1093/nargab/lqaf131). -- Support for raw RNA-seq counts (requires gene lengths). -- **iBreastSubtypeR** refresh: cleaner UX, smarter AUTO guidance, consistent exports. +- Paper published in *NAR Genomics and Bioinformatics* (2025), **Editor's Choice** (DOI: 10.1093/nargab/lqaf131). +- Support for raw RNA-seq counts (requires gene lengths). +- **iBreastSubtypeR** refresh: cleaner UX, smarter AUTO guidance, consistent exports. ### Enhancements -- **ssBC/ssBC.v2: singleton subgroup robustness:** Subgroups with `n=1` no longer error: - - Keeps matrix shape (`drop=FALSE`) and hardens dimnames/types. - - Primary path: original `sspPredict()`.\ - Fallback: nearest-centroid (Spearman) when needed. - - If there are **0 common PAM50 genes**, returns `NA` labels with shaped `distances`/`dist.RORSubtype` to avoid downstream errors. - - ROR computation guarded for incomplete inputs. -- **SSPBC output now "full":** `BS_sspbc()` and Shiny "sspbc" runs return a full metrics table (not calls-only). - - Exports map core label columns to the standard names (`Call_5class` / `Call_4class` when applicable). -- **Shiny: "Load example data…" button** - - One-click load of a small demo dataset from `inst/RshinyTest/` to explore the UI without uploads.\ - - Shows a notification on success; users can immediately run **Preprocess & map** and analyses. -- **AUTO preflight UI (Shiny):** Now detects cohort kind (`TN`, `ER/HER2`, `ER`-only, `HER2`-only) and shows compact stats: - - ER/HER2 subgroups: **ER+/HER2-**, **ER-/HER2-**, **ER+/HER2+**, **ER-/HER2+**\ - - TN cohorts: **TN** vs **nonTN**\ - - Readiness uses the same minimums used by AUTO (sourced programmatically; no duplicated thresholds). -- **Shorter notifications.**\ - -Routine toasts (e.g., "Step 1 complete. Proceed to Step 2.") now auto-dismiss sooner to reduce UI clutter. -- **Phenodata normalization (Mapping):** Accepts flexible ER/HER2/TN encodings and normalizes to canonical forms (`ER+/ER-`, `HER2+/HER2-`, `TN/nonTN`). Ambiguous `HER2="2+"` remains as-is and raises a warning. +- **ssBC/ssBC.v2: singleton subgroup robustness:** Subgroups with `n=1` no longer error: + - Keeps matrix shape (`drop=FALSE`) and hardens dimnames/types. + - Primary path: original `sspPredict()`. + Fallback: nearest-centroid (Spearman) when needed. + - If there are **0 common PAM50 genes**, returns `NA` labels with shaped `distances`/`dist.RORSubtype` to avoid downstream errors. + - ROR computation guarded for incomplete inputs. +- **SSPBC output now "full":** `BS_sspbc()` and Shiny "sspbc" runs return a full metrics table (not calls-only). + - Exports map core label columns to the standard names (`Call_5class` / `Call_4class` when applicable). +- **Shiny: "Load example data..." button** + - One-click load of a small demo dataset from `inst/RshinyTest/` to explore the UI without uploads. + - Shows a notification on success; users can immediately run **Preprocess & map** and analyses. +- **AUTO preflight UI (Shiny):** Now detects cohort kind (`TN`, `ER/HER2`, `ER`-only, `HER2`-only) and shows compact stats: + - ER/HER2 subgroups: **ER+/HER2-**, **ER-/HER2-**, **ER+/HER2+**, **ER-/HER2+** + - TN cohorts: **TN** vs **nonTN** + - Readiness uses the same minimums used by AUTO (sourced programmatically; no duplicated thresholds). +- **Shorter notifications.** + - Routine toasts (e.g., "Step 1 complete. Proceed to Step 2.") now auto-dismiss sooner to reduce UI clutter. +- **Phenodata normalization (Mapping):** Accepts flexible ER/HER2/TN encodings and normalizes to canonical forms (`ER+/ER-`, `HER2+/HER2-`, `TN/nonTN`). Ambiguous `HER2="2+"` remains as-is and raises a warning. ### Bug fixes -- **TN cohorts + ssBC**: `BS_Multi()` now respects TN cohorts when methods are specified manually; -`ssBC`/`ssBC.v2` switch to `s = "TN"` / `"TN.v2"` when a `TN` column indicates a TN cohort. -Falls back to `s = "ER"` / `"ER.v2"` otherwise. -- **AUTO**: Fixed a crash in BS_Multi(methods="AUTO") when ER and/or HER2 contained missing values (NA). -- **AUTO internals**: fixed variable name typo (`samples_ERHER2.icd`). -- **Mapping():** Robust ENTREZID coercion (from `as.character()` to `as.integer()` with suppressed warnings). -- **cIHC.itr**: `outList$distances` now returned as numeric matrix. +- **TN cohorts + ssBC**: `BS_Multi()` now respects TN cohorts when methods are specified manually; `ssBC`/`ssBC.v2` switch to `s = "TN"` / `"TN.v2"` when a `TN` column indicates a TN cohort. Falls back to `s = "ER"` / `"ER.v2"` otherwise. +- **AUTO**: Fixed a crash in `BS_Multi(methods = "AUTO")` when ER and/or HER2 contained missing values (NA). +- **AUTO internals**: fixed variable name typo (`samples_ERHER2.icd`). +- **Mapping():** Robust ENTREZID coercion (from `as.character()` to `as.integer()` with suppressed warnings). +- **cIHC.itr**: `outList$distances` now returned as numeric matrix. ### Shiny -- **Surface method warnings as toasts:** - - Runs are wrapped in a warning handler; package warnings (e.g., ssBC.v2 singleton fallbacks) appear as yellow notifications.\ - - Warnings include subgroup, `n`, and example sample IDs for quick triage. -- **Shiny preflight reset**: - - Fixed a stale cohort summary after switching data sources (manual uploads ↔ example). The preflight panel now revalidates once inputs change. +- **Surface method warnings as toasts:** + - Runs are wrapped in a warning handler; package warnings (e.g., ssBC.v2 singleton fallbacks) appear as yellow notifications. + - Warnings include subgroup, `n`, and example sample IDs for quick triage. +- **Shiny preflight reset**: + - Fixed a stale cohort summary after switching data sources (manual uploads <-> example). The preflight panel now revalidates once inputs change. ### Developer notes -- Added lightweight internal logger `._msg()` and replaced scattered `message()` calls in AUTO to standardize package output without affecting CRAN/Bioc checks. +- Added lightweight internal logger `._msg()` and replaced scattered `message()` calls in AUTO to standardize package output without affecting CRAN/Bioc checks. ### Documentation -- **README/vignette:** brief note on the example-data button and expected file locations. -- **Mapping(): Column metadata clarified.** Added explicit requirements for receptor fields used by AUTO and ER/HER2/TN-dependent methods (`ssBC`, `cIHC`/`cIHC.itr`, `PCAPAM50`) and for ROR covariates (`TSIZE`, `NODE` as numeric 0/1). Documented preferred coding and automatic normalization behavior. +- **README/vignette:** brief note on the example-data button and expected file locations. +- **Mapping(): Column metadata clarified.** Added explicit requirements for receptor fields used by AUTO and ER/HER2/TN-dependent methods (`ssBC`, `cIHC`/`cIHC.itr`, `PCAPAM50`) and for ROR covariates (`TSIZE`, `NODE` as numeric 0/1). Documented preferred coding and automatic normalization behavior. ### Compatibility Notes -- SSPBC "full" output keeps previous columns for calls; additional metrics may appear. +- SSPBC "full" output keeps previous columns for calls; additional metrics may appear. ## Upgrade Notes -- Raw RNA-seq counts are supported **from v1.1.3 onward** (requires gene lengths). -- If you previously parsed `BS` / `BS.Subtype`, switch to **`Call_5class` / `Call_4class`**. -- Package API unchanged. +- Raw RNA-seq counts are supported **from v1.1.3 onward** (requires gene lengths). +- If you previously parsed `BS` / `BS.Subtype`, switch to **`Call_5class` / `Call_4class`**. +- Package API unchanged. diff --git a/R/BreastSubtypeR_methods.R b/R/BreastSubtypeR_methods.R index 66033d9..bac5f02 100755 --- a/R/BreastSubtypeR_methods.R +++ b/R/BreastSubtypeR_methods.R @@ -89,9 +89,10 @@ NULL #' - **Column metadata**: sample-level metadata in `colData()`. Specifically: #' - `PatientID` (**required**). #' - **Receptor fields required by specific methods** (and by `BS_Multi()` / AUTO): -#' * `ER` — required for `ssBC`, `cIHC` / `cIHC.itr`, `PCAPAM50`, and AUTO. -#' * `HER2` — required for `ssBC` with `s="ER.v2"` and for AUTO. -#' * `TN` — required for `ssBC` with `s="TN"` / `"TN.v2"` and for AUTO when TN logic is used. +#' * `ER` - required for `ssBC`, `cIHC` / `cIHC.itr`, `PCAPAM50`, and AUTO. +#' * `HER2` - required for `ssBC` with `s="ER.v2"` and for AUTO. +#' * `TN` - per-sample annotation required for `ssBC` with `s="TN"` / `"TN.v2"`; +#' AUTO uses TNBC-specific cohort handling only when all evaluable TN annotations indicate TN. #' - **Clinical covariates for ROR (NC-based methods only)**: #' * `TSIZE`: Tumor size (0 = \eqn{\le 2}{<= 2} cm; 1 = \eqn{> 2}{> 2} cm). Must be numeric (0/1). #' * `NODE`: Lymph node status (0 = negative; \eqn{\ge 1}{>= 1} = positive). Must be numeric (0/1). @@ -1109,14 +1110,15 @@ BS_Multi <- function(data_input, # Detect true TN cohort has_TN_col <- "TN" %in% colnames(pheno) - is_TN_cohort <- has_TN_col && all(na.omit(pheno$TN) == "TN") && nrow(pheno) > 0 + tn_summary <- if (has_TN_col) .tn_only_summary(pheno$TN) else NULL + is_TN_cohort <- has_TN_col && isTRUE(tn_summary$is_tn_only) # manual vs AUTO is_manual <- !(length(methods) == 1 && methods[1] == "AUTO") # Only tell users in MANUAL mode that ssBC routing will use TN/TN.v2 if (is_manual && is_TN_cohort && any(methods %in% c("ssBC", "ssBC.v2"))) { - n_tn <- sum(na.omit(pheno$TN) == "TN") + n_tn <- tn_summary$n_tn .msg("Detected pure TN cohort (TN=100%%, n=%d). Routing ssBC with s='TN' and ssBC.v2 with s='TN.v2'.", n_tn, origin = "MANUAL" diff --git a/R/Utilities.R b/R/Utilities.R index e5bf9a5..a18fb55 100755 --- a/R/Utilities.R +++ b/R/Utilities.R @@ -212,6 +212,25 @@ prepare_ssp_matrix <- function(x, genes.s, RawCounts, samplenames, verbose) { df } +#' Summarize TN annotations for cohort-level TN-only detection +#' +#' @keywords internal +#' @noRd +.tn_only_summary <- function(tn_flag) { + tn_chr <- toupper(trimws(as.character(tn_flag))) + tn_evaluable <- !is.na(tn_chr) & nzchar(tn_chr) + tn_truthy <- tn_chr %in% c("TN", "TRUE", "T", "YES", "Y", "1") + + n_tn <- sum(tn_truthy & tn_evaluable) + n_evaluable_tn <- sum(tn_evaluable) + + list( + n_tn = n_tn, + n_evaluable_tn = n_evaluable_tn, + is_tn_only = n_evaluable_tn > 0L && n_tn == n_evaluable_tn + ) +} + #' Map Gene IDs and Handle missing data #' @@ -376,11 +395,11 @@ get_methods <- function(pheno) { ## ---- TNBC handling ------------------------------------------------ if ("TN" %in% colnames(pheno)) { - # Accept common truthy markers in TN (case-insensitive) - tn_flag <- toupper(trimws(as.character(pheno$TN))) - n_TN <- sum(tn_flag %in% c("TN", "TRUE", "T", "YES", "Y", "1"), na.rm = TRUE) + # Use TNBC cohort handling only when all evaluable TN annotations are TN. + tn_summary <- .tn_only_summary(pheno$TN) + n_TN <- tn_summary$n_tn - if (n_TN > 0) { + if (tn_summary$is_tn_only) { cohort.select <- "TNBC" if (n_TN >= n_TN_threshold) { @@ -392,8 +411,11 @@ get_methods <- function(pheno) { .msg("Running methods: AIMS, sspbc", origin = "AUTO") methods <- c("AIMS", "sspbc") } + } else if (tn_summary$n_evaluable_tn > 0L) { + .msg("TN column detected but not all evaluable samples are TN; + proceeding with non-TNBC logic.", origin = "AUTO") } else { - .msg("TN column detected but no samples flagged as TN; + .msg("TN column detected but no evaluable samples flagged as TN; proceeding with non-TNBC logic.", origin = "AUTO") } } diff --git a/man/BreastSubtypeR.Rd b/man/BreastSubtypeR.Rd index 1f25d77..69a8e62 100755 --- a/man/BreastSubtypeR.Rd +++ b/man/BreastSubtypeR.Rd @@ -57,6 +57,7 @@ methods; FPKM for SSP-based methods. Authors: \itemize{ + \item Qiao Yang \email{yq.kiuo@gmail.com} (\href{https://orcid.org/0000-0002-4098-3246}{ORCID}) \item Emmanouil G. Sifakis \email{emmanouil.sifakis@ki.se} (\href{https://orcid.org/0000-0001-9919-4471}{ORCID}) } diff --git a/man/Mapping.Rd b/man/Mapping.Rd index b8eb856..4df1a03 100755 --- a/man/Mapping.Rd +++ b/man/Mapping.Rd @@ -33,9 +33,10 @@ renamed as \code{probe}. \item \code{PatientID} (\strong{required}). \item \strong{Receptor fields required by specific methods} (and by \code{BS_Multi()} / AUTO): \itemize{ -\item \code{ER} — required for \code{ssBC}, \code{cIHC} / \code{cIHC.itr}, \code{PCAPAM50}, and AUTO. -\item \code{HER2} — required for \code{ssBC} with \code{s="ER.v2"} and for AUTO. -\item \code{TN} — required for \code{ssBC} with \code{s="TN"} / \code{"TN.v2"} and for AUTO when TN logic is used. +\item \code{ER} - required for \code{ssBC}, \code{cIHC} / \code{cIHC.itr}, \code{PCAPAM50}, and AUTO. +\item \code{HER2} - required for \code{ssBC} with \code{s="ER.v2"} and for AUTO. +\item \code{TN} - per-sample annotation required for \code{ssBC} with \code{s="TN"} / \code{"TN.v2"}; +AUTO uses TNBC-specific cohort handling only when all evaluable TN annotations indicate TN. } \item \strong{Clinical covariates for ROR (NC-based methods only)}: \itemize{ diff --git a/tests/testthat/test-tn-cohort-detection.R b/tests/testthat/test-tn-cohort-detection.R new file mode 100644 index 0000000..7b4b524 --- /dev/null +++ b/tests/testthat/test-tn-cohort-detection.R @@ -0,0 +1,87 @@ +make_pheno <- function(er, her2, tn = NULL) { + pheno <- data.frame( + PatientID = paste0("S", seq_along(er)), + ER = er, + HER2 = her2, + stringsAsFactors = FALSE + ) + if (!is.null(tn)) { + pheno$TN <- tn + } + rownames(pheno) <- pheno$PatientID + pheno +} + +expect_auto_panel <- function(pheno, methods, cohort) { + old <- options(BreastSubtypeR.verbose = FALSE) + on.exit(options(old), add = TRUE) + out <- BreastSubtypeR:::get_methods(pheno) + expect_identical(out$methods, methods) + expect_identical(out$cohort.select, cohort) +} + +tn_panel <- c("ssBC", "ssBC.v2", "AIMS", "sspbc") +full_mixed_panel <- c( + "parker.original", "genefu.scale", "genefu.robust", + "ssBC", "ssBC.v2", "cIHC", "cIHC.itr", "PCAPAM50", "AIMS", "sspbc" +) + +test_that("TN-only cohorts with a TN column select the TNBC panel", { + pheno <- make_pheno( + er = rep("ER-", 20), + her2 = rep("HER2-", 20), + tn = rep("TN", 20) + ) + + expect_auto_panel(pheno, tn_panel, "TNBC") +}) + +test_that("mixed cohorts with some TN samples are not treated as TNBC", { + pheno <- make_pheno( + er = c(rep("ER+", 50), rep("ER-", 30)), + her2 = c(rep("HER2-", 30), rep("HER2+", 20), rep("HER2-", 20), rep("HER2+", 10)), + tn = c(rep("nonTN", 60), rep("TN", 20)) + ) + + expect_auto_panel(pheno, full_mixed_panel, "mixed") +}) + +test_that("UNC-like mixed cohorts with TN annotations are not treated as TNBC", { + pheno <- make_pheno( + er = c(rep("ER+", 62), rep("ER-", 38)), + her2 = c(rep("HER2+", 9), rep("HER2-", 53), rep("HER2+", 6), rep("HER2-", 32)), + tn = c(rep("nonTN", 70), rep("TN", 30)) + ) + + expect_auto_panel(pheno, full_mixed_panel, "mixed") +}) + +test_that("missing or empty TN values do not trigger TNBC handling", { + pheno <- make_pheno( + er = c(rep("ER+", 25), rep("ER-", 25)), + her2 = c(rep("HER2-", 20), rep("HER2+", 5), rep("HER2-", 20), rep("HER2+", 5)), + tn = rep(c(NA_character_, "", " "), length.out = 50) + ) + + expect_auto_panel(pheno, full_mixed_panel, "mixed") +}) + +test_that("partly missing TN values with TN and nonTN remain non-TNBC", { + pheno <- make_pheno( + er = c(rep("ER+", 25), rep("ER-", 25)), + her2 = c(rep("HER2-", 20), rep("HER2+", 5), rep("HER2-", 20), rep("HER2+", 5)), + tn = c(rep("TN", 10), rep("nonTN", 10), rep(NA_character_, 10), rep("", 10), rep(" ", 10)) + ) + + expect_auto_panel(pheno, full_mixed_panel, "mixed") +}) + +test_that("partly missing TN values select TNBC when all evaluable values are TN", { + pheno <- make_pheno( + er = rep("ER-", 20), + her2 = rep("HER2-", 20), + tn = c(rep("TN", 18), NA_character_, "") + ) + + expect_auto_panel(pheno, tn_panel, "TNBC") +})