Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions DESCRIPTION
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down Expand Up @@ -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
90 changes: 49 additions & 41 deletions NEWS.md
Original file line number Diff line number Diff line change
@@ -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), **Editors 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.
12 changes: 7 additions & 5 deletions R/BreastSubtypeR_methods.R
Original file line number Diff line number Diff line change
Expand Up @@ -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).
Expand Down Expand Up @@ -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"
Expand Down
32 changes: 27 additions & 5 deletions R/Utilities.R
Original file line number Diff line number Diff line change
Expand Up @@ -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
#'
Expand Down Expand Up @@ -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) {
Expand All @@ -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")
}
}
Expand Down
1 change: 1 addition & 0 deletions man/BreastSubtypeR.Rd

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

7 changes: 4 additions & 3 deletions man/Mapping.Rd

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

87 changes: 87 additions & 0 deletions tests/testthat/test-tn-cohort-detection.R
Original file line number Diff line number Diff line change
@@ -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")
})
Loading