| Title: | Guarded and Auditable Discovery of Prognostic RNA-Seq Signatures |
|---|---|
| Description: | Orchestrates the statistical pipeline underlying prognostic biomarker discovery from bulk RNA-seq SummarizedExperiment objects: sample-identity and quality-control auditing, confounder and design-formula detection prior to differential expression, signature construction with explicit cross-validation stability reporting, survival modeling (Kaplan-Meier, log-rank, Cox proportional hazards), and external validation against public cohorts. Every automated statistical decision is reported with its supporting statistic and a plain-language rationale, and validation stages are gated rather than silently degraded when guardrail criteria are not met. The package records all guardrail decisions, flag ledgers, and validation results in a structured audit trail that can be rendered into a standalone HTML report. |
| Authors: | Abdelkarim Hani Ghrieb [aut, cre] |
| Maintainer: | Abdelkarim Hani Ghrieb <[email protected]> |
| License: | MIT + file LICENSE |
| Version: | 0.99.19 |
| Built: | 2026-10-02 17:46:25 UTC |
| Source: | https://github.com/bioc/rnaSentry |
build_signature() is the core signature-construction stage of the
rnaSentry pipeline. From a SummarizedExperiment with per-sample
survival metadata it:
Screens every variable gene with a univariate Cox proportional-hazards model against the event indicator, recording the hazard ratio, Wald p-value and a Benjamini-Hochberg adjusted p-value.
Selects the signature genes by the most significant
univariate p-values (method = "top_n") or by an adjusted
p-value threshold (method = "p_value").
Estimates joint coefficients by refitting a single multivariate Cox model on the selected genes. Genes whose coefficient is not finite (rank deficiency) are dropped and the model refit; the signature is never silently degraded, the drop is reported as a flag.
Reports stability with repeated, event-stratified k-fold
cross-validation: within each fold the coefficients are re-estimated on
the training samples and the concordance index of the resulting risk
score is evaluated on the held-out samples. The mean and standard
deviation of the per-fold concordance are returned as
cv_summary. Concordance is Harrell's C in the risk-score
convention: a higher risk score is concordant with an earlier event.
build_signature( se, time_col, event_col, outcome_col = "overall_survival", method = c("top_n", "p_value"), top_n = 20, p_threshold = 0.05, repeats = 5, folds = 5, seed = NULL, design_terms = character(0), adjust_for_design = TRUE, min_events_per_parameter = 5, BPPARAM = NULL )build_signature( se, time_col, event_col, outcome_col = "overall_survival", method = c("top_n", "p_value"), top_n = 20, p_threshold = 0.05, repeats = 5, folds = 5, seed = NULL, design_terms = character(0), adjust_for_design = TRUE, min_events_per_parameter = 5, BPPARAM = NULL )
se |
A |
time_col |
Character. Column of |
event_col |
Character. Column of |
outcome_col |
Character. A display label for the outcome, e.g.
|
method |
Character. Selection rule, one of |
top_n |
Integer. Number of genes to keep when |
p_threshold |
Numeric in (0, 1). Adjusted p-value cutoff when
|
repeats |
Integer. Number of cross-validation repeats. Defaults to
|
folds |
Integer. Number of folds per repeat. Defaults to |
seed |
Optional integer. Seeds the cross-validation fold shuffling for
reproducible results. Seeding is scoped with |
design_terms |
Character vector of |
adjust_for_design |
Logical. When |
min_events_per_parameter |
Numeric. Minimum acceptable number of
events per signature gene. When the cohort has fewer events per final
signature gene, the signature is flagged with an
|
BPPARAM |
Optional |
Reading the cross-validated concordance (important). The univariate
screening that selects the signature runs on the full cohort before
the cross-validation split, so the CV fold is not fully untouched by
the selection step. On large candidate panels the resulting concordance is
therefore optimistic (screening-internal): even data with permuted
survival yields a non-trivial null concordance (e.g. ~0.8 on a
~21k-gene panel, driven by winner's-curse selection), far above the 0.5 a
fully out-of-sample estimate would give. Treat cv_summary as a
screening-internal stability metric and interpret it relative to a
matched null — a permutation-null or random-gene control — rather than
against 0.5 in absolute terms. The only fully independent out-of-sample
estimate is validate_external() on a separate cohort.
The return value has class "rnaSentry_signature" and is the input
expected by the survival modeling and validation stages
(km_curve(), cox_model(), survival_parametric(),
validate_external()) and by lock_signature(). The signature
is returned unlocked; downstream stages that must not be rerun after
publication can be protected with lock_signature().
An object of class "rnaSentry_signature" (a list) with
elements:
Character vector of the final signature genes.
The outcome_col display label.
The metadata columns used.
Character vector of the design terms actually used to adjust univariate screening (empty when screening was unadjusted).
Data.frame with one row per screened gene (columns
gene, HR, p, adj_p).
Named numeric vector of joint Cox coefficients for the signature genes.
Data.frame with columns repeat_id,
fold and c_index (concordance on held-out samples).
Named numeric vector c(mean, sd) of the
per-fold concordance.
List with method and n_genes.
Always FALSE; set by lock_signature().
Provenance timestamps.
Analysis details.
Data.frame summarizing every issue raised, with columns
check, severity, detail and stage.
The signature is a linear Cox risk score (larger score = earlier event).
The pipeline assumes standard right-censored survival from bulk RNA-seq
with raw integer counts or a correctly-named logcounts (RNA-seq) or
exprs (microarray) assay
(pre-scaled data in a counts-named slot is flagged as
possibly_log_scaled). It does not model competing risks,
time-varying covariates, left truncation, single-cell data, or
non-survival end points, and it never corrects batch or design effects.
In underpowered cohorts (fewer than min_events_per_parameter
events per signature gene) the returned concordance and hazard ratios are
unstable and an events_per_parameter warning is raised; treat such
results as hypothesis-generating rather than confirmatory. The discovery
stage additionally refuses cohorts with fewer than two events; the
downstream survival stages (km_curve,
cox_model, survival_parametric,
validate_external) use a one-event minimum and instead
surface instability through their sparse_events guardrail.
library(SummarizedExperiment) set.seed(5) counts <- matrix(rpois(400, lambda = 500), nrow = 20, ncol = 20, dimnames = list(paste0("gene", 1:20), paste0("S", 1:20))) sig_expr <- colMeans(counts[1:5, , drop = FALSE]) risk <- scale(sig_expr)[, 1] * 0.4 event_time <- rexp(20, rate = 0.03 * exp(0.8 * risk)) censor_time <- rexp(20, rate = 0.02) time <- pmin(event_time, censor_time) event <- as.integer(event_time < censor_time) coldata <- S4Vectors::DataFrame(time = time, event = event, condition = factor(rep(c("A", "B"), 10)), row.names = colnames(counts)) se <- SummarizedExperiment(assays = list(counts = counts), colData = coldata) sig <- build_signature(se, time_col = "time", event_col = "event", top_n = 5, repeats = 1, folds = 2, seed = 1) siglibrary(SummarizedExperiment) set.seed(5) counts <- matrix(rpois(400, lambda = 500), nrow = 20, ncol = 20, dimnames = list(paste0("gene", 1:20), paste0("S", 1:20))) sig_expr <- colMeans(counts[1:5, , drop = FALSE]) risk <- scale(sig_expr)[, 1] * 0.4 event_time <- rexp(20, rate = 0.03 * exp(0.8 * risk)) censor_time <- rexp(20, rate = 0.02) time <- pmin(event_time, censor_time) event <- as.integer(event_time < censor_time) coldata <- S4Vectors::DataFrame(time = time, event = event, condition = factor(rep(c("A", "B"), 10)), row.names = colnames(counts)) se <- SummarizedExperiment(assays = list(counts = counts), colData = coldata) sig <- build_signature(se, time_col = "time", event_col = "event", top_n = 5, repeats = 1, folds = 2, seed = 1) sig
cox_model() fits a single multivariate Cox model of the survival
outcome on the signature genes together with any design terms recorded in
the signature at build time (sig$design_terms) and any extra
confounders supplied here. It is the companion to the gene-only
scoring model in build_signature: the signature's risk score
deliberately ignores the design covariates so it stays portable, while this
stage supplies inference adjusted for them on a cohort that records
them.
cox_model(sig, se, confounders = character(0))cox_model(sig, se, confounders = character(0))
sig |
An object of class |
se |
A |
confounders |
Character vector of additional |
For every fitted term the model reports the hazard ratio with a Wald
confidence interval and p-value. For the signature genes it also reports a
Benjamini-Hochberg adjusted p-value across the genes, so that gene-level
multiplicity is controlled. Proportional-hazards diagnostics
(cox.zph) are run on every term and the GLOBAL test;
a violation flags the model.
The stage is read-only: it works on both locked and unlocked signatures and records the signature's lock state in its output.
An object of class "rnaSentry_cox_model" (a list) with
elements:
The signature genes in the model.
All fitted terms: the genes, then sig$design_terms,
then confounders.
The model formula.
The coxph fit object.
Data.frame with one row per fitted term: term,
coefficient, se, HR, HR_low,
HR_high and p.
Data.frame of the signature-gene rows of
coef_table with an added BH-adjusted adj_p.
Data.frame from cox.zph with one row per term
plus GLOBAL; NULL when the test could not be run.
Harrell's concordance index of the fitted model.
Logical; TRUE when any term (including
GLOBAL) violates proportional hazards at alpha 0.05.
Number of samples and events used by the fit.
Whether the input signature was locked.
Provenance timestamp.
Data.frame of issues raised (missing covariate values,
non-estimable genes, proportional-hazards violations), with columns
check, severity, detail and stage.
The model assumes proportional hazards (tested and flagged via
cox.zph) and standard right-censored survival. It does not model
competing risks, time-varying covariates, or left truncation. Like the
rest of the pipeline it makes no batch-effect correction; adjustment for
recorded design covariates is supplied through sig$design_terms
and confounders.
library(SummarizedExperiment) set.seed(7) counts <- matrix(rpois(450, lambda = 500), nrow = 30, ncol = 15, dimnames = list(paste0("gene", 1:30), paste0("S", 1:15))) sig_expr <- colMeans(counts[1:4, , drop = FALSE]) risk <- scale(sig_expr)[, 1] * 0.4 event_time <- rexp(15, rate = 0.03 * exp(0.8 * risk)) censor_time <- rexp(15, rate = 0.02) time <- pmin(event_time, censor_time) event <- as.integer(event_time < censor_time) coldata <- S4Vectors::DataFrame(time = time, event = event, batch = factor(rep(c("B1", "B2"), length.out = 15)), row.names = colnames(counts)) se <- SummarizedExperiment(assays = list(counts = counts), colData = coldata) sig <- build_signature(se, "time", "event", top_n = 4, repeats = 1, folds = 2, seed = 1, design_terms = "batch") cm <- cox_model(sig, se) cm plot(cm)library(SummarizedExperiment) set.seed(7) counts <- matrix(rpois(450, lambda = 500), nrow = 30, ncol = 15, dimnames = list(paste0("gene", 1:30), paste0("S", 1:15))) sig_expr <- colMeans(counts[1:4, , drop = FALSE]) risk <- scale(sig_expr)[, 1] * 0.4 event_time <- rexp(15, rate = 0.03 * exp(0.8 * risk)) censor_time <- rexp(15, rate = 0.02) time <- pmin(event_time, censor_time) event <- as.integer(event_time < censor_time) coldata <- S4Vectors::DataFrame(time = time, event = event, batch = factor(rep(c("B1", "B2"), length.out = 15)), row.names = colnames(counts)) se <- SummarizedExperiment(assays = list(counts = counts), colData = coldata) sig <- build_signature(se, "time", "event", top_n = 4, repeats = 1, folds = 2, seed = 1, design_terms = "batch") cm <- cox_model(sig, se) cm plot(cm)
Given a SummarizedExperiment, design_audit() examines a set of
candidate design variables (technical or biological covariates such as
batch, sequencing lane or tissue) and recommends a design formula for
downstream differential-expression analysis. It does this without fitting
per-gene models: the leading principal component of the expression data
serves as a surrogate for global expression structure, and every candidate
variable is tested for association with that surrogate. A variable that
tracks the surrogate significantly is flagged as a potential confounder,
because omitting it from the design risks attributing expression structure
to the variable of interest.
design_audit( se, design_vars, outcome_col = NULL, surrogate_pc = 1, alpha = 0.05, interaction_alpha = alpha, redundant_effect_size = 0.8 )design_audit( se, design_vars, outcome_col = NULL, surrogate_pc = 1, alpha = 0.05, interaction_alpha = alpha, redundant_effect_size = 0.8 )
se |
A |
design_vars |
Character vector naming candidate design variables, all
of which must be columns of |
outcome_col |
Optional character. Name of the biological variable of
interest (for example condition), also a column of |
surrogate_pc |
Integer. Which principal component to use as the
surrogate for global expression structure. Defaults to |
alpha |
Numeric in (0, 1). Significance threshold for the confounder
and pairwise association tests. Defaults to |
interaction_alpha |
Numeric in (0, 1). Significance threshold for the
interaction likelihood-ratio test. Defaults to |
redundant_effect_size |
Numeric in (0, 1). Minimum effect size
(Cramer's V, absolute Pearson r or eta-squared) for a significant
pairwise association to be declared redundant. Defaults to |
Three passes are performed:
Confounder scan: each candidate variable is tested against
the surrogate expression gradient (surrogate_pc). Categorical
variables are tested with one-way ANOVA (or Kruskal-Wallis when the
scores are not approximately normal) and numeric variables with linear
regression; effect sizes are reported as eta-squared,
epsilon-squared or R-squared.
Redundancy scan: every pair of variables (including
outcome_col when supplied) is cross-tested with Cramer's V
(categorical-categorical), Pearson correlation (numeric-numeric) or a
one-way test (mixed), and pairs whose association is significant and
whose effect size reaches redundant_effect_size are declared
redundant.
Interaction scan: a likelihood-ratio test compares
other_pc ~ surrogate + v with other_pc ~ surrogate * v
for each candidate variable, where other_pc is the first PC that
is not the surrogate. A significant interaction means the variable's
influence on expression structure changes along the surrogate gradient;
this is reported as interaction-type confounding rather than silently
folded into the recommended formula.
The recommended formula is ~ outcome_col + design_variables,
with variables declared redundant against a later-listed variable dropped.
Every statistical decision is returned alongside a plain-language
rationale; nothing is dropped or included silently.
An object of class "rnaSentry_design" (a list) with
elements:
The recommended design formula as a
stats::formula object.
Character version of formula.
Character vector of plain-language statements justifying every automated decision.
Data.frame with one row per candidate variable
(columns variable, type, n, test,
statistic, df, p_value, adj_p,
effect_size, effect_size_type, flagged). The
p_value column holds the raw association p-value; adj_p
is the Benjamini-Hochberg adjusted p-value across all tested
candidate variables, and flagged is based on adj_p.
Logical; whether the interaction LRT could be performed (it requires at least two PCs and enough samples).
Data.frame with one row per candidate variable
(columns variable, test, statistic, df,
p_value, flagged), or NULL when not tested.
Data.frame with one row per variable pair
(columns var1, var2, type_pair, test,
statistic, df, p_value, effect_size,
effect_size_type, redundant), or NULL when fewer
than two variables are available.
Character vector of design variables removed from the recommended formula as redundant.
The surrogate PC actually used (after clamping to the available number of PCs).
The thresholds used.
Data.frame summarizing every issue raised, with columns
check, severity, detail and stage.
library(SummarizedExperiment) set.seed(4) counts <- matrix(rpois(360, lambda = 200), nrow = 30, ncol = 12, dimnames = list(paste0("gene", 1:30), paste0("S", 1:12))) batch <- factor(rep(c("B1", "B2"), each = 6)) condition <- factor(rep(c("A", "B"), times = 6)) counts[1:10, batch == "B2"] <- counts[1:10, batch == "B2"] * 3L coldata <- S4Vectors::DataFrame(batch = batch, condition = condition, age = runif(12, 40, 80), row.names = paste0("S", 1:12)) se <- SummarizedExperiment(assays = list(counts = counts), colData = coldata) design_audit(se, design_vars = c("batch", "age"), outcome_col = "condition")library(SummarizedExperiment) set.seed(4) counts <- matrix(rpois(360, lambda = 200), nrow = 30, ncol = 12, dimnames = list(paste0("gene", 1:30), paste0("S", 1:12))) batch <- factor(rep(c("B1", "B2"), each = 6)) condition <- factor(rep(c("A", "B"), times = 6)) counts[1:10, batch == "B2"] <- counts[1:10, batch == "B2"] * 3L coldata <- S4Vectors::DataFrame(batch = batch, condition = condition, age = runif(12, 40, 80), row.names = paste0("S", 1:12)) se <- SummarizedExperiment(assays = list(counts = counts), colData = coldata) design_audit(se, design_vars = c("batch", "age"), outcome_col = "condition")
Downloads GSE20685 (Li *et al.*, 2010) from GEO via
GEOquery::getGEO() (which caches the series matrix itself),
performs probe-to-gene collapse (largest mean expression per symbol),
subsets to the top 3000 most variable genes, and returns a
SummarizedExperiment with an exprs assay (microarray log2
intensities, used as-is by the pipeline) and clinical metadata
(time, event, age, subtype).
fetch_gse20685()fetch_gse20685()
This helper packages the processing pipeline from
inst/scripts/repro_gse20685.R for use in vignettes. The full
series matrix is downloaded from GEO, probes are collapsed to gene symbols
by largest mean expression, and the top 3000 most variable genes are
retained. Clinical metadata includes overall-survival follow-up time
(years), event indicator (1 = death), age at diagnosis, and breast-cancer
subtype.
A SummarizedExperiment or NULL if the download or
processing fails (e.g. network unavailable, GEOquery not
installed).
se <- fetch_gse20685() sese <- fetch_gse20685() se
generate_report() assembles the results of any collection of
rnaSentry stages into a single self-contained HTML report. It renders the
plain report_template.Rmd shipped with the package (via
render), which prints a pipeline summary, a unified
audit-trail table collecting every flag from every supplied stage (all
rnaSentry stage results carry a flags data.frame with
check/severity/detail/stage columns), and one
section per stage using its print method.
generate_report( stages, output_file = "rnaSentry_report.html", output_dir = ".", quiet = TRUE )generate_report( stages, output_file = "rnaSentry_report.html", output_dir = ".", quiet = TRUE )
stages |
A named list of rnaSentry stage results (for example
elements of the |
output_file |
Character. File name (without directory) of the rendered
report. Defaults to |
output_dir |
Character. Directory to write the report into. Must exist. Defaults to the working directory. |
quiet |
Logical. Passed to |
The path of the rendered report, invisibly.
library(SummarizedExperiment) set.seed(9) counts <- matrix(rpois(400, lambda = 500), nrow = 20, ncol = 20, dimnames = list(paste0("gene", 1:20), paste0("S", 1:20))) sig_expr <- colMeans(counts[1:5, , drop = FALSE]) risk <- scale(sig_expr)[, 1] * 0.4 event_time <- rexp(20, rate = 0.03 * exp(0.8 * risk)) censor_time <- rexp(20, rate = 0.02) time <- pmin(event_time, censor_time) event <- as.integer(event_time < censor_time) coldata <- S4Vectors::DataFrame(time = time, event = event, row.names = colnames(counts)) se <- SummarizedExperiment(assays = list(counts = counts), colData = coldata) sig <- build_signature(se, "time", "event", top_n = 5, repeats = 1, folds = 2, seed = 1) stages <- list( km_curve = km_curve(sig, se), cox_model = cox_model(sig, se) ) generate_report(stages, output_dir = tempdir())library(SummarizedExperiment) set.seed(9) counts <- matrix(rpois(400, lambda = 500), nrow = 20, ncol = 20, dimnames = list(paste0("gene", 1:20), paste0("S", 1:20))) sig_expr <- colMeans(counts[1:5, , drop = FALSE]) risk <- scale(sig_expr)[, 1] * 0.4 event_time <- rexp(20, rate = 0.03 * exp(0.8 * risk)) censor_time <- rexp(20, rate = 0.02) time <- pmin(event_time, censor_time) event <- as.integer(event_time < censor_time) coldata <- S4Vectors::DataFrame(time = time, event = event, row.names = colnames(counts)) se <- SummarizedExperiment(assays = list(counts = counts), colData = coldata) sig <- build_signature(se, "time", "event", top_n = 5, repeats = 1, folds = 2, seed = 1) stages <- list( km_curve = km_curve(sig, se), cox_model = cox_model(sig, se) ) generate_report(stages, output_dir = tempdir())
Scores each sample of a SummarizedExperiment with the signature's
joint Cox coefficients and splits the cohort into "low" and
"high" risk groups. By default the split is at the median risk
score of the supplied cohort; pass cutpoint to re-apply a cutpoint
established elsewhere (for example the discovery-cohort median recorded in
a previous km_curve() call), so risk groups are comparable across
cohorts. Fits a Kaplan-Meier survival curve for each group with a log-rank
test. The analysis is read-only: it works on both locked and unlocked
signatures and records the signature's lock state in its output.
km_curve(sig, se, cutpoint = NULL)km_curve(sig, se, cutpoint = NULL)
sig |
An object of class |
se |
A |
cutpoint |
Numeric risk-score threshold: samples with score
|
An object of class "rnaSentry_km" (a list) with elements:
The signature genes scored.
Per-sample risk score (linear predictor).
Factor with levels "low" and "high"
from the risk-score split.
The risk-score cutpoint applied.
"median" when the cohort median was used,
"custom" when cutpoint was supplied.
The survfit object with one curve per group.
p-value from the log-rank test.
Named vector of median survival per group.
Event counts per group.
Whether the input signature was locked.
Provenance timestamp.
Data.frame of issues raised (e.g. sparse events per
group), with columns check, severity, detail and
stage.
library(SummarizedExperiment) set.seed(5) counts <- matrix(rpois(120, lambda = 500), nrow = 12, ncol = 10, dimnames = list(paste0("gene", 1:12), paste0("S", 1:10))) sig_expr <- colMeans(counts[1:3, , drop = FALSE]) risk <- scale(sig_expr)[, 1] * 0.4 event_time <- rexp(10, rate = 0.03 * exp(0.8 * risk)) censor_time <- rexp(10, rate = 0.02) time <- pmin(event_time, censor_time) event <- as.integer(event_time < censor_time) coldata <- S4Vectors::DataFrame(time = time, event = event, row.names = colnames(counts)) se <- SummarizedExperiment(assays = list(counts = counts), colData = coldata) sig <- build_signature(se, time_col = "time", event_col = "event", top_n = 3, repeats = 1, folds = 2, seed = 1) km <- km_curve(sig, se) kmlibrary(SummarizedExperiment) set.seed(5) counts <- matrix(rpois(120, lambda = 500), nrow = 12, ncol = 10, dimnames = list(paste0("gene", 1:12), paste0("S", 1:10))) sig_expr <- colMeans(counts[1:3, , drop = FALSE]) risk <- scale(sig_expr)[, 1] * 0.4 event_time <- rexp(10, rate = 0.03 * exp(0.8 * risk)) censor_time <- rexp(10, rate = 0.02) time <- pmin(event_time, censor_time) event <- as.integer(event_time < censor_time) coldata <- S4Vectors::DataFrame(time = time, event = event, row.names = colnames(counts)) se <- SummarizedExperiment(assays = list(counts = counts), colData = coldata) sig <- build_signature(se, time_col = "time", event_col = "event", top_n = 3, repeats = 1, folds = 2, seed = 1) km <- km_curve(sig, se) km
Thin validation wrapper around
SummarizedExperiment::SummarizedExperiment() that catches the most
common input mistakes before they propagate into the pipeline: duplicate or
missing sample IDs, non-integer counts, empty rows or columns, and
non-syntactic gene identifiers.
load_counts(counts, colData, assay_name = "counts", check_gene_ids = TRUE)load_counts(counts, colData, assay_name = "counts", check_gene_ids = TRUE)
counts |
Integer or numeric matrix of gene expression values (genes in rows, samples in columns). Row names must be gene identifiers; column names must be sample identifiers. |
colData |
A |
assay_name |
Character. Name for the assay slot. Defaults to
|
check_gene_ids |
Logical. When |
A SummarizedExperiment with one assay named
assay_name. Any issues found are recorded in a flag ledger
accessible via S4Vectors::metadata(se)$flags and surfaced via
warning().
This function validates structure; it does not normalise, filter, or
transform the count matrix. Gene identifiers are checked but not converted.
Sample-metadata columns used later by the pipeline (e.g. time_col,
event_col, batch_col) are not validated here — that happens
in the stage that first uses them.
counts <- matrix(1:12, nrow = 3, dimnames = list(c("TP53", "BRCA1", "MYC"), c("S1", "S2", "S3", "S4"))) coldata <- data.frame( time = c(10, 12, 8, 15), event = c(1, 0, 1, 0), row.names = colnames(counts)) se <- load_counts(counts, coldata) secounts <- matrix(1:12, nrow = 3, dimnames = list(c("TP53", "BRCA1", "MYC"), c("S1", "S2", "S3", "S4"))) coldata <- data.frame( time = c(10, 12, 8, 15), event = c(1, 0, 1, 0), row.names = colnames(counts)) se <- load_counts(counts, coldata) se
Toggles the reproducibility guardrail on an rnaSentry_signature
object. Locking records a timestamp and an audit entry in the signature's
flags table; downstream stages that must not be rerun after
publication can then be protected. The function is copy-on-write: the
input object is never modified and the updated signature is returned.
lock_signature(sig, lock = TRUE)lock_signature(sig, lock = TRUE)
sig |
An object of class |
lock |
Logical. |
A new rnaSentry_signature object with updated
locked, lock_time and flags fields.
Locking stores a gene-set fingerprint in a session-level environment.
While this fingerprint is set, run_rnaSentry() will refuse to
run, preventing silent re-selection of genes after survival analysis.
The lock can be reversed by calling lock_signature(sig, lock = FALSE),
but doing so after survival stages have been run is not recommended.
sig <- structure(list(genes = "gene1", outcome = "overall_survival", locked = FALSE), class = "rnaSentry_signature") locked <- lock_signature(sig) locked$locked locked$lock_time unlocked <- lock_signature(locked, lock = FALSE) unlocked$lockedsig <- structure(list(genes = "gene1", outcome = "overall_survival", locked = FALSE), class = "rnaSentry_signature") locked <- lock_signature(sig) locked$locked locked$lock_time unlocked <- lock_signature(locked, lock = FALSE) unlocked$locked
Runs principal component analysis on a variance-stabilized or
log-transformed version of the count assay and reports how much variance
each principal component (PC) explains. When batch_col is supplied,
each PC score is tested against the batch variable and any PC where batch
explains a significant share of variance is flagged. This feeds
design_audit() by surfacing which batch/technical variables
are associated with global expression structure.
pca_audit( se, batch_col = NULL, top_n_pcs = 5, scale = TRUE, batch_alpha = 0.05 )pca_audit( se, batch_col = NULL, top_n_pcs = 5, scale = TRUE, batch_alpha = 0.05 )
se |
A |
batch_col |
Optional character. Name of a |
top_n_pcs |
Integer. Number of leading PCs to include in
|
scale |
Logical. Whether to scale each gene to unit variance before
PCA ( |
batch_alpha |
Numeric in (0, 1). Significance threshold used to flag a
PC whose score associates with |
The analysis matrix is chosen by preference from an existing assay named
"logcounts" or "vst" (RNA-seq) or "exprs"
(microarray); if none exists, log2(counts +
1) is computed from the first assay. Genes with zero variance or missing
values are removed because they carry no information for PCA and would
make stats::prcomp fail; the number removed and the reason are
reported in the result (nothing is dropped silently).
Unlike design_audit, no multiple-testing correction is
applied to the per-PC batch tests: batch_alpha is applied to each
test at its nominal level. This is a deliberate choice, because at most
top_n_pcs (default 5) PCs are tested against a single batch
variable, so the correction would be a near no-op; the raw p-values and a
Benjamini-Hochberg adjusted p-value (adj_p) are both reported in
batch_tests so users can apply their own threshold. Candidate
confounder screening in design_audit, which tests many
variables at once, is BH-corrected instead.
An object of class "rnaSentry_pca" (a list) with elements:
The stats::prcomp object.
Named numeric vector of the percent of variance explained by every PC.
A data.frame of the first top_n_pcs PC scores,
with sample IDs as row names.
The colData(se) columns as a data.frame, kept so
plot_pca_audit can color points by a metadata column.
Name of the assay used for the analysis.
Number of genes retained after filtering.
Number of genes removed before PCA.
Character describing why genes were removed (empty string if none).
The batch column name used, or NULL.
A data.frame with one row per tested PC (columns
pc, test, statistic, df, p_value,
adj_p, effect_size, effect_size_type,
flagged), or NULL when batch_col is not
supplied.
A data.frame summarizing every issue raised, with columns
check, severity, detail and stage.
library(SummarizedExperiment) set.seed(1) counts <- matrix(rpois(240, lambda = 200), nrow = 20, ncol = 12, dimnames = list(paste0("gene", 1:20), paste0("S", 1:12))) coldata <- DataFrame(condition = rep(c("A", "B"), each = 6), batch = factor(rep(c("B1", "B2"), each = 6)), row.names = paste0("S", 1:12)) se <- SummarizedExperiment(assays = list(counts = counts), colData = coldata) pca_audit(se, batch_col = "batch")library(SummarizedExperiment) set.seed(1) counts <- matrix(rpois(240, lambda = 200), nrow = 20, ncol = 12, dimnames = list(paste0("gene", 1:20), paste0("S", 1:12))) coldata <- DataFrame(condition = rep(c("A", "B"), each = 6), batch = factor(rep(c("B1", "B2"), each = 6)), row.names = paste0("S", 1:12)) se <- SummarizedExperiment(assays = list(counts = counts), colData = coldata) pca_audit(se, batch_col = "batch")
Produces a ggplot2 scatter plot of two principal component score
axes, optionally colored by a sample-metadata column. This is a purely
visual convenience: the statistical results returned by
pca_audit do not depend on this function being called.
plot_pca_audit(pca_result, pc_x = 1, pc_y = 2, color_by = NULL)plot_pca_audit(pca_result, pc_x = 1, pc_y = 2, color_by = NULL)
pca_result |
An object of class |
pc_x, pc_y
|
Integers selecting the two PC axes to plot. Defaults to
|
color_by |
Optional character. Name of a sample-metadata column in
the original |
A ggplot object.
library(SummarizedExperiment) set.seed(2) counts <- matrix(rpois(240, lambda = 200), nrow = 20, ncol = 12, dimnames = list(paste0("gene", 1:20), paste0("S", 1:12))) coldata <- DataFrame(batch = factor(rep(c("B1", "B2"), each = 6)), row.names = paste0("S", 1:12)) se <- SummarizedExperiment(assays = list(counts = counts), colData = coldata) res <- pca_audit(se, batch_col = "batch") plot_pca_audit(res)library(SummarizedExperiment) set.seed(2) counts <- matrix(rpois(240, lambda = 200), nrow = 20, ncol = 12, dimnames = list(paste0("gene", 1:20), paste0("S", 1:12))) coldata <- DataFrame(batch = factor(rep(c("B1", "B2"), each = 6)), row.names = paste0("S", 1:12)) se <- SummarizedExperiment(assays = list(counts = counts), colData = coldata) res <- pca_audit(se, batch_col = "batch") plot_pca_audit(res)
Runs a battery of pre-analysis checks on a raw-counts
SummarizedExperiment: duplicate
sample identifiers, missing values in sample metadata, non-integer
count values, and library-size outliers detected via a robust
(median absolute deviation-based) rule. Every check is reported with
the statistic that triggered it, not just a pass/fail flag, so the
result can be inspected or logged rather than trusted blindly.
qc_explore(se, mad_threshold = 3)qc_explore(se, mad_threshold = 3)
se |
A |
mad_threshold |
Numeric scalar. Samples whose log10 library size
deviates from the median by more than |
An object of class "rnaSentry_qc" (a list) with elements:
Number of samples (columns) in se.
Number of genes (rows) in se.
Character vector of duplicated sample IDs,
or character(0) if none.
Logical; TRUE if the count assay
contains non-integer values.
A data.frame reporting the proportion of
missing values per colData(se) column.
Character vector of sample IDs flagged as library-size outliers.
A data.frame summarizing every issue raised, with
columns check, severity, detail and stage.
library(SummarizedExperiment) set.seed(1) counts <- matrix(rpois(60, lambda = 200), nrow = 6, ncol = 10, dimnames = list(paste0("gene", 1:6), paste0("S", 1:10))) counts[, 1] <- counts[, 1] * 50L # inject one library-size outlier coldata <- DataFrame(condition = rep(c("A", "B"), each = 5), row.names = paste0("S", 1:10)) se <- SummarizedExperiment(assays = list(counts = counts), colData = coldata) qc <- qc_explore(se) qc$flagslibrary(SummarizedExperiment) set.seed(1) counts <- matrix(rpois(60, lambda = 200), nrow = 6, ncol = 10, dimnames = list(paste0("gene", 1:6), paste0("S", 1:10))) counts[, 1] <- counts[, 1] * 50L # inject one library-size outlier coldata <- DataFrame(condition = rep(c("A", "B"), each = 5), row.names = paste0("S", 1:10)) se <- SummarizedExperiment(assays = list(counts = counts), colData = coldata) qc <- qc_explore(se) qc$flags
run_rnaSentry() executes the full rnaSentry pipeline on a single
cohort and collects every stage result into one object:
design_audit over design_vars (when any are
supplied).
pca_audit (with batch_col when supplied).
build_signature, then lock_signature so
the survival-modeling stages operate on the published signature.
km_curve, cox_model and
survival_parametric on the locked signature.
generate_report assembling all stages into an HTML
report (when render_report = TRUE).
run_rnaSentry( se, time_col, event_col, outcome_col = "overall_survival", design_terms = character(0), design_vars = design_terms, batch_col = NULL, method = c("top_n", "p_value"), top_n = 20, p_threshold = 0.05, repeats = 5, folds = 5, seed = NULL, adjust_for_design = TRUE, min_events_per_parameter = 5, BPPARAM = NULL, report_file = "rnaSentry_report.html", report_dir = ".", render_report = TRUE )run_rnaSentry( se, time_col, event_col, outcome_col = "overall_survival", design_terms = character(0), design_vars = design_terms, batch_col = NULL, method = c("top_n", "p_value"), top_n = 20, p_threshold = 0.05, repeats = 5, folds = 5, seed = NULL, adjust_for_design = TRUE, min_events_per_parameter = 5, BPPARAM = NULL, report_file = "rnaSentry_report.html", report_dir = ".", render_report = TRUE )
se |
A |
time_col |
Character. Column of |
event_col |
Character. Column of |
outcome_col |
Character. Display label for the outcome. Defaults to
|
design_terms |
Character vector of |
design_vars |
Character vector of |
batch_col |
Optional character. |
method, top_n, p_threshold, repeats, folds, seed, adjust_for_design, min_events_per_parameter, BPPARAM
|
Passed to |
report_file |
Character. Output file name for the HTML report. |
report_dir |
Character. Directory (which must exist) to write the report into. |
render_report |
Logical. When |
The result's stages list can be passed directly to
generate_report, or any stage can be re-run individually
(for example validate_external against an independent
cohort, using the cutpoint recorded by the km_curve stage).
An object of class "rnaSentry_run" (a list) with elements:
Named list of the stage results, with entries
design_audit (only when design_vars was supplied),
pca_audit, build_signature, km_curve,
cox_model and survival_parametric.
Path of the rendered HTML report, or NULL when
render_report = FALSE.
The run locks the discovered signature (see lock_signature),
recording a session-level fingerprint. While any locked signature exists,
run_rnaSentry() refuses to run again, preventing silent re-selection
of signature genes after survival analysis. To run the pipeline a second
time in the same session (for example on another cohort), release the lock
first with lock_signature(sig, lock = FALSE) on the locked
signature. Individual stages remain re-runnable while the lock is set.
The pipeline targets bulk RNA-seq with standard right-censored survival
and linear Cox risk scores (see the Assumptions sections of
build_signature and cox_model). It detects and
reports batch and design confounders but does not correct for them, and it
makes no differential-expression calls. On small cohorts an
events_per_parameter warning from build_signature is
expected and should be interpreted as a call for caution, not ignored.
library(SummarizedExperiment) set.seed(8) counts <- matrix(rpois(400, lambda = 500), nrow = 20, ncol = 20, dimnames = list(paste0("gene", 1:20), paste0("S", 1:20))) sig_expr <- colMeans(counts[1:5, , drop = FALSE]) risk <- scale(sig_expr)[, 1] * 0.4 event_time <- rexp(20, rate = 0.03 * exp(0.8 * risk)) censor_time <- rexp(20, rate = 0.02) time <- pmin(event_time, censor_time) event <- as.integer(event_time < censor_time) coldata <- S4Vectors::DataFrame(time = time, event = event, batch = factor(rep(c("B1", "B2"), 10)), row.names = colnames(counts)) se <- SummarizedExperiment(assays = list(counts = counts), colData = coldata) run <- run_rnaSentry(se, "time", "event", design_vars = "batch", top_n = 5, repeats = 1, folds = 2, seed = 1, report_dir = tempdir()) runlibrary(SummarizedExperiment) set.seed(8) counts <- matrix(rpois(400, lambda = 500), nrow = 20, ncol = 20, dimnames = list(paste0("gene", 1:20), paste0("S", 1:20))) sig_expr <- colMeans(counts[1:5, , drop = FALSE]) risk <- scale(sig_expr)[, 1] * 0.4 event_time <- rexp(20, rate = 0.03 * exp(0.8 * risk)) censor_time <- rexp(20, rate = 0.02) time <- pmin(event_time, censor_time) event <- as.integer(event_time < censor_time) coldata <- S4Vectors::DataFrame(time = time, event = event, batch = factor(rep(c("B1", "B2"), 10)), row.names = colnames(counts)) se <- SummarizedExperiment(assays = list(counts = counts), colData = coldata) run <- run_rnaSentry(se, "time", "event", design_vars = "batch", top_n = 5, repeats = 1, folds = 2, seed = 1, report_dir = tempdir()) run
Compares the sex recorded in sample metadata against sex inferred from expression of XIST and a panel of Y-chromosome marker genes. Designed to catch sample swaps and metadata entry errors before they propagate into downstream differential expression or survival analysis.
sex_check( se, sex_col = "sex", xist_gene = "XIST", y_genes = c("RPS4Y1", "DDX3Y", "KDM5D") )sex_check( se, sex_col = "sex", xist_gene = "XIST", y_genes = c("RPS4Y1", "DDX3Y", "KDM5D") )
se |
A |
sex_col |
Character. Name of the |
xist_gene |
Character. Gene symbol for XIST. Defaults to
|
y_genes |
Character vector of Y-chromosome marker gene symbols.
Defaults to |
Inference rule: a sample is called "F" when XIST expression is
high relative to the Y-gene panel, "M" when the reverse holds,
and "ambiguous" when neither signal is clearly dominant (for
example, low-quality or highly degraded samples). The decision uses
the sign of each sample's XIST-minus-Y-panel rank score: a score at
least one rank unit above zero is called "F", at least one rank
unit below zero "M", and a score within one rank unit of zero
"ambiguous". Because the rule is based on ranks rather than
hard-coded expression cutoffs, it is robust across different
normalizations and sequencing depths.
A data.frame with one row per sample and columns
sample_id, reported_sex, inferred_sex, and
status (one of "OK", "MISMATCH",
"AMBIGUOUS", or "MISSING" when reported sex is absent).
library(SummarizedExperiment) genes <- c("XIST", "RPS4Y1", "DDX3Y", "KDM5D", "GAPDH") samples <- paste0("S", 1:6) expr <- matrix(0, nrow = length(genes), ncol = length(samples), dimnames = list(genes, samples)) # samples 1-3 look female (high XIST, low Y genes) expr["XIST", 1:3] <- c(950, 900, 875) expr[c("RPS4Y1", "DDX3Y", "KDM5D"), 1:3] <- 5 # samples 4-6 look male, but S6's metadata will be mislabeled below expr["XIST", 4:6] <- 5 expr[c("RPS4Y1", "DDX3Y", "KDM5D"), 4:6] <- 400 expr["GAPDH", ] <- 1000 coldata <- DataFrame(sex = c("F", "F", "F", "M", "M", "F"), row.names = samples) # S6 mislabeled as F se <- SummarizedExperiment(assays = list(counts = expr), colData = coldata) sex_check(se)library(SummarizedExperiment) genes <- c("XIST", "RPS4Y1", "DDX3Y", "KDM5D", "GAPDH") samples <- paste0("S", 1:6) expr <- matrix(0, nrow = length(genes), ncol = length(samples), dimnames = list(genes, samples)) # samples 1-3 look female (high XIST, low Y genes) expr["XIST", 1:3] <- c(950, 900, 875) expr[c("RPS4Y1", "DDX3Y", "KDM5D"), 1:3] <- 5 # samples 4-6 look male, but S6's metadata will be mislabeled below expr["XIST", 4:6] <- 5 expr[c("RPS4Y1", "DDX3Y", "KDM5D"), 4:6] <- 400 expr["GAPDH", ] <- 1000 coldata <- DataFrame(sex = c("F", "F", "F", "M", "M", "F"), row.names = samples) # S6 mislabeled as F se <- SummarizedExperiment(assays = list(counts = expr), colData = coldata) sex_check(se)
survival_parametric() scores each sample of a
SummarizedExperiment with the signature's joint Cox coefficients and
fits a set of parametric accelerated-failure-time models of the event time
on that score, using survreg. It returns an AIC
comparison across the fitted distributions, the fitted survival curves
(evaluated at the mean risk score) for overlay on the Kaplan-Meier
estimate, and a plot() method that draws the KM curve together with
the parametric curves.
survival_parametric( sig, se, dists = c("weibull", "exponential", "lognormal", "loglogistic") )survival_parametric( sig, se, dists = c("weibull", "exponential", "lognormal", "loglogistic") )
sig |
An object of class |
se |
A |
dists |
Character vector of parametric distributions to fit, any
subset of |
The stage is read-only: it works on both locked and unlocked signatures and records the signature's lock state in its output.
An object of class "rnaSentry_parametric" (a list) with
elements:
The signature genes scored.
Per-sample risk score (linear predictor).
Named list of survreg fits, one per distribution.
Data.frame comparing distributions on loglik,
npar, AIC, delta_AIC, weight and
best, ordered by increasing AIC.
Name of the lowest-AIC distribution.
The survfit object of the whole cohort.
Named list of fitted survival curves (data.frame with
time and survival), evaluated at the mean risk score.
Whether the input signature was locked.
Provenance timestamp.
Data.frame of issues raised (e.g. a model that failed to
fit, or near-tied AIC values), with columns check,
severity, detail and stage.
library(SummarizedExperiment) set.seed(9) counts <- matrix(rpois(450, lambda = 500), nrow = 30, ncol = 15, dimnames = list(paste0("gene", 1:30), paste0("S", 1:15))) sig_expr <- colMeans(counts[1:4, , drop = FALSE]) risk <- scale(sig_expr)[, 1] * 0.4 event_time <- rexp(15, rate = 0.03 * exp(0.8 * risk)) censor_time <- rexp(15, rate = 0.02) time <- pmin(event_time, censor_time) event <- as.integer(event_time < censor_time) coldata <- S4Vectors::DataFrame(time = time, event = event, row.names = colnames(counts)) se <- SummarizedExperiment(assays = list(counts = counts), colData = coldata) sig <- build_signature(se, "time", "event", top_n = 4, repeats = 1, folds = 2, seed = 1) sp <- survival_parametric(sig, se) sp plot(sp)library(SummarizedExperiment) set.seed(9) counts <- matrix(rpois(450, lambda = 500), nrow = 30, ncol = 15, dimnames = list(paste0("gene", 1:30), paste0("S", 1:15))) sig_expr <- colMeans(counts[1:4, , drop = FALSE]) risk <- scale(sig_expr)[, 1] * 0.4 event_time <- rexp(15, rate = 0.03 * exp(0.8 * risk)) censor_time <- rexp(15, rate = 0.02) time <- pmin(event_time, censor_time) event <- as.integer(event_time < censor_time) coldata <- S4Vectors::DataFrame(time = time, event = event, row.names = colnames(counts)) se <- SummarizedExperiment(assays = list(counts = counts), colData = coldata) sig <- build_signature(se, "time", "event", top_n = 4, repeats = 1, folds = 2, seed = 1) sp <- survival_parametric(sig, se) sp plot(sp)
validate_external() applies a locked-down signature to an
independent cohort: it scores every external sample with the signature's
joint Cox coefficients (the gene-only model, so the score is portable to
cohorts that do not record the discovery cohort's design covariates), splits
the cohort into "low" and "high" risk groups at the supplied
discovery cutpoint, and reports the external discriminant validity:
a log-rank test between the groups, per-group median survival and event
counts, and the concordance index of the continuous risk score against the
external survival outcome (Harrell's C in the risk-score convention: a
higher risk score is concordant with an earlier event).
validate_external( sig, external_se, time_col = NULL, event_col = NULL, cutpoint, min_gene_overlap_frac = 1, drop_missing = FALSE )validate_external( sig, external_se, time_col = NULL, event_col = NULL, cutpoint, min_gene_overlap_frac = 1, drop_missing = FALSE )
sig |
An object of class |
external_se |
A |
time_col, event_col
|
Names of the survival columns in
|
cutpoint |
The risk-score threshold defining the |
min_gene_overlap_frac |
Numeric in (0, 1]. The minimum fraction of
signature genes that must be present in the external cohort when
|
drop_missing |
Logical. When |
The stage never recomputes a within-cohort cutpoint. The
cutpoint must be passed explicitly (for example the discovery-cohort
median recorded in the cutpoint element returned by
km_curve); passing no cutpoint is an error, so external risk
groups are always defined by the same threshold that defined the discovery
groups.
Missing genes are handled strictly by default: drop_missing = FALSE
and min_gene_overlap_frac = 1 (the defaults) require every signature
gene to be present, and error otherwise, naming the missing genes. Set
drop_missing = TRUE (with min_gene_overlap_frac < 1 as
needed) to explicitly allow scoring on a subset of genes; the dropped genes
are named in a flag.
The stage is read-only: it works on both locked and unlocked signatures and records the signature's lock state in its output.
An object of class "rnaSentry_external" (a list) with
elements:
The signature genes present in the external cohort.
The signature genes absent from the external cohort (empty when none).
Per-sample external risk score.
Factor with levels "low" and "high"
from the discovery cutpoint.
The applied cutpoint and "custom".
The survfit object with one curve per group.
p-value from the log-rank test between groups.
Named vector of median survival per group.
Event counts per group.
Concordance index of the continuous risk score against the external survival outcome.
Whether the input signature was locked.
Provenance timestamp.
Data.frame of issues raised (dropped genes, sparse
events, non-finite concordance), with columns check,
severity, detail and stage.
External risk groups are defined exclusively by the supplied discovery
cutpoint; the stage never re-estimates a threshold from the external
cohort. The cohort is assumed to be bulk RNA-seq with standard
right-censored survival and the same assay convention as the discovery
data (raw counts or a correctly-named logcounts assay; pre-scaled
data in a counts-named slot is flagged as
possibly_log_scaled). Competing risks and other non-standard
survival structures are not modeled. Unlike the discovery stage
(build_signature, which requires at least two events), a
one-event minimum applies here; the resulting instability is reported
through the sparse_events guardrail.
library(SummarizedExperiment) set.seed(11) counts <- matrix(rpois(800, lambda = 500), nrow = 40, ncol = 20, dimnames = list(paste0("gene", 1:40), paste0("S", 1:20))) sig_expr <- colMeans(counts[1:5, , drop = FALSE]) risk <- scale(sig_expr)[, 1] * 0.4 event_time <- rexp(20, rate = 0.03 * exp(0.8 * risk)) censor_time <- rexp(20, rate = 0.02) time <- pmin(event_time, censor_time) event <- as.integer(event_time < censor_time) coldata <- S4Vectors::DataFrame(time = time, event = event, row.names = colnames(counts)) se <- SummarizedExperiment(assays = list(counts = counts), colData = coldata) sig <- build_signature(se, "time", "event", top_n = 5, repeats = 1, folds = 2, seed = 1) cutoff <- km_curve(sig, se)$cutpoint ext <- se ext <- ext[, sample(ncol(ext))] val <- validate_external(sig, ext, cutpoint = cutoff) vallibrary(SummarizedExperiment) set.seed(11) counts <- matrix(rpois(800, lambda = 500), nrow = 40, ncol = 20, dimnames = list(paste0("gene", 1:40), paste0("S", 1:20))) sig_expr <- colMeans(counts[1:5, , drop = FALSE]) risk <- scale(sig_expr)[, 1] * 0.4 event_time <- rexp(20, rate = 0.03 * exp(0.8 * risk)) censor_time <- rexp(20, rate = 0.02) time <- pmin(event_time, censor_time) event <- as.integer(event_time < censor_time) coldata <- S4Vectors::DataFrame(time = time, event = event, row.names = colnames(counts)) se <- SummarizedExperiment(assays = list(counts = counts), colData = coldata) sig <- build_signature(se, "time", "event", top_n = 5, repeats = 1, folds = 2, seed = 1) cutoff <- km_curve(sig, se)$cutpoint ext <- se ext <- ext[, sample(ncol(ext))] val <- validate_external(sig, ext, cutpoint = cutoff) val