| Title: | BRAF-RAS Score (BRS) Classifier for Papillary Thyroid Carcinoma |
|---|---|
| Description: | Computes the BRAF-RAS Score (BRS), a 71-gene expression-based signature that quantifies whether a papillary thyroid carcinoma (PTC) sample resembles a BRAF-V600E-mutant or a RAS-mutant transcriptional profile, following Agrawal et al. (2014) <doi:10.1016/j.cell.2014.09.050>, Extended Experimental Procedures, section 14.1. It is a re-implementation from that description rather than the original code, which was never released: it reproduces the published classification (99.7 percent against driver-mutation status under cross-validation) but not the exact published score values. The original study only scored the subset of samples for which both exome and RNA sequencing data were available, since exome data was needed to define the two reference groups (but not to score additional samples). This package lets you fit the reference centroids from any cohort with known BRAF-mutant/RAS-mutant labels and then score new samples — including ones with expression data only — using just their gene expression. Expression can be supplied as a matrix, a SummarizedExperiment or an ExpressionSet. |
| Authors: | Camila Barrios-Cervantes [aut, cph] (ORCID: <https://orcid.org/0009-0006-9971-790X>), Daniel Pérez-Calixto [aut, cph] (ORCID: <https://orcid.org/0009-0004-3988-1260>), Hugo Tovar [aut, cre, cph] (ORCID: <https://orcid.org/0000-0002-8360-6133>), Instituto Nacional de Medicina Genómica [fnd] (ROR: <https://ror.org/01qjckx08>, project: INMEGEN-2026-14 (Proyecto 81)) |
| Maintainer: | Hugo Tovar <[email protected]> |
| License: | MIT + file LICENSE |
| Version: | 0.99.7 |
| Built: | 2026-10-10 01:08:43 UTC |
| Source: | https://github.com/bioc/thyroidBRS |
Computes the two reference centroids (BRAF-mutant and RAS-mutant) needed to
score the BRAF-RAS Score (BRS) of Agrawal et al. (2014), from a cohort where
at least some samples have a known driver mutation. The original study
needed exome sequencing to assign those labels, but that is only required
once, to fit the centroids. Scoring additional samples afterwards
(predict.brs_fit()) only needs their gene expression.
brs_fit(expr, labels, genes = NULL, log2_transform = FALSE, assay = NULL) ## S3 method for class 'brs_fit' print(x, ...)brs_fit(expr, labels, genes = NULL, log2_transform = FALSE, assay = NULL) ## S3 method for class 'brs_fit' print(x, ...)
expr |
Gene expression, features (genes) as rows and samples as
columns: a numeric matrix, a
SummarizedExperiment::SummarizedExperiment or a
|
labels |
A named character vector giving the reference group for the
samples that have one, or a single string naming a column of the
object's |
genes |
Character vector of gene symbols to use as the signature.
Defaults to the alias-resolved names in brs_genes (70 genes; |
log2_transform |
Logical. If |
assay |
Name or index of the assay to use when |
x |
A |
... |
Ignored; present for consistency with |
An object of class "brs_fit", a list with:
Signature genes actually present in expr and used
to fit the centroids.
Requested signature genes not found in expr.
Genes dropped because they had zero variance across the reference samples (uninformative for distance).
Genes dropped because they had a missing
value in at least one reference sample. Together with
genes_zero_variance these account for every requested gene that
was found but not used.
Per-gene mean/SD from the reference samples,
used to z-score any sample later in predict.brs_fit().
Mean z-score profile of the BRAF-mutant and RAS-mutant reference samples.
Number of reference samples in each group.
Names of the samples used to fit, so that
validate_brs() can tell resubstitution from real validation.
The scale the centroids were fitted on.
In Agrawal et al. the two reference sets are defined by driver mutation: set B is the BRAF-V600E-mutant tumors and set R is the RAS-mutant tumors. They are not the BRAF-like/RAS-like classes that the BRS itself produces.
This distinction matters when working with the published TCGA-THCA tables.
The column BRAF_RAF_class (also exposed by
TCGAbiolinks::TCGAquery_subtype()) is exactly sign(BRAF_RAF_score):
it is the output of the classifier.
Fitting centroids on it is circular: it pulls 43 tumors with other drivers
and 1 RAS-mutant tumor into the "BRAF" centroid, and 68 other-driver tumors
into the "RAS" centroid. Use the mutation column (BRAFV600E_RAS,
restricted to BRAF_V600E and RAS) to fit, and keep BRAF_RAF_class /
BRAF_RAF_score for validation. See vignette("thyroidBRS").
Agrawal N, Akbani R, Aksoy BA, et al. "Integrated Genomic Characterization of Papillary Thyroid Carcinoma." Cell. 2014;159(3):676-690. doi:10.1016/j.cell.2014.09.050, Extended Experimental Procedures section 14.1.
predict.brs_fit() to score samples with a fitted object,
validate_brs() to check concordance against known labels.
set.seed(1) genes <- na.omit(brs_genes$current_symbol)[1:10] samples <- paste0("S", 1:20) group <- rep(c("BRAF_V600E", "RAS"), each = 10) # Give the two groups the difference the signature is meant to detect, so # the example shows the classifier working rather than fitting noise. shift <- ifelse( brs_genes$up_in[match(genes, brs_genes$current_symbol)] == "RAS", 1, -1 ) expr <- matrix(rnorm(length(genes) * length(samples)), nrow = length(genes), dimnames = list(genes, samples) ) expr <- expr + outer(shift, ifelse(group == "BRAF_V600E", -0.5, 0.5)) labels <- setNames(group[1:16], samples[1:16]) fit <- brs_fit(expr, labels, genes = genes) fitset.seed(1) genes <- na.omit(brs_genes$current_symbol)[1:10] samples <- paste0("S", 1:20) group <- rep(c("BRAF_V600E", "RAS"), each = 10) # Give the two groups the difference the signature is meant to detect, so # the example shows the classifier working rather than fitting noise. shift <- ifelse( brs_genes$up_in[match(genes, brs_genes$current_symbol)] == "RAS", 1, -1 ) expr <- matrix(rnorm(length(genes) * length(samples)), nrow = length(genes), dimnames = list(genes, samples) ) expr <- expr + outer(shift, ifelse(group == "BRAF_V600E", -0.5, 0.5)) labels <- setNames(group[1:16], samples[1:16]) fit <- brs_fit(expr, labels, genes = genes) fit
The 71-gene expression signature used to compute the BRAF-RAS Score (BRS)
for papillary thyroid carcinoma (PTC), as published in Agrawal et al.
(2014), Figure S7A. No machine-readable gene list was released with the
paper, so the symbols were transcribed from the published heatmap and then
verified against it row by row. The provenance and the checks that were
run are written up in data-raw/brs_genes.md, which is in the package
repository rather than the installed package:
https://github.com/camilasauria/thyroidBRS/blob/main/data-raw/brs_genes.md.
brs_genesbrs_genes
A data frame with 71 rows and 4 columns:
Gene symbol exactly as it appears in the 2014 publication's Figure S7A.
Current HGNC-approved symbol, resolved against
org.Hs.eg.db. Identical to original_symbol for genes whose symbol
has not changed. NA for FLJ23867, which does not map to any symbol
or alias in current annotation and should be treated as permanently
missing from the signature. brs_fit() and predict.brs_fit()
drop it.
Which of the two row-clusters of Figure S7A the gene belongs
to: 1 for the 13-gene block, 2 for the 58-gene block. Each block is
listed alphabetically in the figure, which is what makes the
transcription checkable: no gene is out of order within its block.
Which reference group the gene is higher in, "RAS" for
block 1 and "BRAF" for block 2. Refitting the centroids on TCGA-THCA
recovers this split exactly: all 13 block-1 genes have a higher RAS
centroid and all 57 usable block-2 genes a higher BRAF centroid, with
no exceptions. That is an independent check on the gene list: a
mis-transcribed symbol would land in the wrong block.
Four symbols in the 2014 figure are no longer the approved HGNC symbol:
FAM176A (now EVA1A), PVRL4 (NECTIN4), TM7SF4 (DCSTAMP) and
ARNTL (BMAL1). Expression matrices built from current GENCODE/HGNC
annotation use the new symbols, so matching on original_symbol silently
loses those genes. Use current_symbol, which is what brs_fit() does by
default. Resolution was checked against org.Hs.eg.db; re-run
data-raw/check_symbols.R when the annotation is updated. data-raw/ is
not shipped in the installed package; it lives in the package repository,
https://github.com/camilasauria/thyroidBRS.
Agrawal N, Akbani R, Aksoy BA, et al. "Integrated Genomic Characterization of Papillary Thyroid Carcinoma." Cell. 2014;159(3):676-690. doi:10.1016/j.cell.2014.09.050, Figure S7A ("mRNA expression of the 71 genes used to derive the BRAF-V600E-RAS score (BRS) across 391 PTC samples").
brs_genes # Genes actually usable today (alias-resolved, FLJ23867 excluded): na.omit(brs_genes$current_symbol) # The two blocks of Figure S7A: table(brs_genes$block)brs_genes # Genes actually usable today (alias-resolved, FLJ23867 excluded): na.omit(brs_genes$current_symbol) # The two blocks of Figure S7A: table(brs_genes$block)
Convenience wrapper equivalent to
predict(brs_fit(expr, labels, ...), newdata), for the common case of
scoring the same cohort you used to fit the centroids (e.g. to extend a
partially-labeled cohort to full coverage).
brs_score( expr, labels, newdata = expr, genes = NULL, log2_transform = FALSE, standardize = c("reference", "cohort", "rank"), assay = NULL )brs_score( expr, labels, newdata = expr, genes = NULL, log2_transform = FALSE, standardize = c("reference", "cohort", "rank"), assay = NULL )
expr |
Gene expression, features (genes) as rows and samples as
columns: a numeric matrix, a
SummarizedExperiment::SummarizedExperiment or a
|
labels |
A named character vector giving the reference group for the
samples that have one, or a single string naming a column of the
object's |
newdata |
Expression to score: a matrix, a |
genes |
Character vector of gene symbols to use as the signature.
Defaults to the alias-resolved names in brs_genes (70 genes; |
log2_transform |
Logical. If |
standardize |
Passed to |
assay |
Name or index of the assay to use when |
See predict.brs_fit().
set.seed(1) genes <- na.omit(brs_genes$current_symbol)[1:10] samples <- paste0("S", 1:20) group <- rep(c("BRAF_V600E", "RAS"), each = 10) # Give the two groups the difference the signature is meant to detect, so # the example shows the classifier working rather than fitting noise. shift <- ifelse( brs_genes$up_in[match(genes, brs_genes$current_symbol)] == "RAS", 1, -1 ) expr <- matrix(rnorm(length(genes) * length(samples)), nrow = length(genes), dimnames = list(genes, samples) ) expr <- expr + outer(shift, ifelse(group == "BRAF_V600E", -0.5, 0.5)) labels <- setNames(group[1:16], samples[1:16]) brs_score(expr, labels, genes = genes)set.seed(1) genes <- na.omit(brs_genes$current_symbol)[1:10] samples <- paste0("S", 1:20) group <- rep(c("BRAF_V600E", "RAS"), each = 10) # Give the two groups the difference the signature is meant to detect, so # the example shows the classifier working rather than fitting noise. shift <- ifelse( brs_genes$up_in[match(genes, brs_genes$current_symbol)] == "RAS", 1, -1 ) expr <- matrix(rnorm(length(genes) * length(samples)), nrow = length(genes), dimnames = list(genes, samples) ) expr <- expr + outer(shift, ifelse(group == "BRAF_V600E", -0.5, 0.5)) labels <- setNames(group[1:16], samples[1:16]) brs_score(expr, labels, genes = genes)
brs_fit(), predict() and brs_score() accept expression as a plain
numeric matrix, a SummarizedExperiment::SummarizedExperiment or a
Biobase::ExpressionSet. Whichever you pass, the signature is matched on
row names, so those must be gene symbols.
This page documents the input containers the package accepts
rather than a function of its own. Whichever container the expression
arrives in, brs_fit() returns a "brs_fit" object and
predict.brs_fit() returns a data frame.
A SummarizedExperiment usually carries several assays (for GDC RNA-seq:
unstranded, tpm_unstranded, fpkm_unstranded, ...). Pass the one you
want by name through assay; the first assay is used if you do not, which
for GDC data is raw counts and is almost certainly not what you want.
An ExpressionSet has a single exprs() matrix and ignores assay.
When labels is a bare character string of length one it is looked up as a
column of colData() (or pData()), so brs_fit(se, "driver_mutation")
works directly on an object that carries its own sample annotation. Sample
names come from colnames(). A named vector of length one is a label for
one sample, not a column name.
Whichever column you name, it has to hold driver-mutation status. On
TCGA-THCA the column giving the published BRAF-like/RAS-like class is the
classifier's own output, and fitting on it is circular; see the warning in
brs_fit().
Microarray expression works as input: the signature is well represented
on common arrays and its structure is preserved there. What does not
carry over from one platform to another is the zero threshold that
separates
the two classes, because the per-gene means and SDs stored in a brs_fit
are those of the platform it was fitted on. Fit centroids on the same
platform you are scoring whenever you have driver-mutation labels for it.
Scoring array data against RNA-seq centroids still ranks samples correctly,
but the class call is not trustworthy; predict() warns when every sample
lands on the same side, which is the usual symptom.
Computes the BRAF-RAS Score (BRS) and the resulting class for every sample
in newdata, using the centroids and per-gene mean/SD stored in object
(from brs_fit()). newdata samples do not need to have been part of the
reference cohort used to fit those centroids, and do not need any
mutation/exome information.
## S3 method for class 'brs_fit' predict( object, newdata, log2_transform = NULL, standardize = c("reference", "cohort", "rank"), assay = NULL, ... )## S3 method for class 'brs_fit' predict( object, newdata, log2_transform = NULL, standardize = c("reference", "cohort", "rank"), assay = NULL, ... )
object |
A |
newdata |
Gene expression to score, in any of the forms |
log2_transform |
Logical, or |
standardize |
How to put |
assay |
Assay to use when |
... |
Not used. Present for S3 consistency with |
A data frame with one row per sample in colnames(newdata):
sample, brs_score (normalized distance difference; negative = more
BRAF-like, positive = more RAS-like), brs_scaled (the same score
rescaled to across the scored samples, NA when it cannot
be determined) and brs_class ("Braf-like" or "Ras-like"). A sample
with a missing value in a signature gene gets NA in all three, with a
warning.
The centroids carry the per-gene mean and SD of the cohort they were
fitted on. That is what makes a sample's score its own: it does not move
when you score it alongside a different set of samples. It is also what
breaks when newdata comes from another platform, because those means and
SDs no longer describe it.
Measured on GSE33630 (Affymetrix, 49 papillary carcinomas) against
centroids fitted on TCGA RNA-seq: all three settings rank the tumors
almost identically, at Spearman 0.97, 0.98 and 0.92 against the array's
own signature axis, and they disagree completely on where zero falls.
"reference" calls 49 of 49 BRAF-like; "cohort" calls 42 and 7.
"cohort" and "rank" fix the threshold across platforms by paying for
it elsewhere: each sample's score then depends on which samples it is
scored with. Scoring TCGA subsets of different composition against the
same centroids, "reference" gets 97.5% to 99.6% right whatever the class
mix, while "cohort" drops to 8.4% on a cohort that happens to be all
RAS-like.
So: score on the platform you fitted on and keep the default. Crossing
platforms, prefer fitting new centroids there if you have driver-mutation
labels. Failing that, use these settings for the ranking and treat the
class call as unreliable. predict() warns when every sample lands on the
same side of zero, which is the usual symptom.
Following Agrawal et al. (2014), Extended Experimental Procedures 14.1, the
score of a tumor t is the difference between its distance to the two
centroids,
where the distance is a normalized Euclidean distance. Here v(t) is the
sample's expression vector z-scored gene-by-gene against the reference
cohort, and the norm is divided by the square root of the number of
signature genes, so that scores stay comparable when the gene set shrinks
(for example because some signature genes are absent from newdata).
Negative scores are BRAF-V600E-like and positive scores RAS-like. A score
of exactly 0 is not defined by the paper; it is assigned to "Ras-like"
here, consistent with treating the non-negative half as the RAS side.
The BRS values published for TCGA-THCA are additionally rescaled to
, by dividing the negative scores by the most negative value
and the positive scores by the largest one. brs_scaled reproduces that
rescaling. Note that it is a property of the set of samples being scored,
not of a sample on its own: scoring a different cohort, or a single sample,
gives a different (or undefined) rescaling. Use brs_score when you need a
value that is stable per sample, and brs_scaled when you need a value on
the same axis as the published figures.
set.seed(1) genes <- na.omit(brs_genes$current_symbol)[1:10] samples <- paste0("S", 1:20) group <- rep(c("BRAF_V600E", "RAS"), each = 10) # Give the two groups the difference the signature is meant to detect, so # the example shows the classifier working rather than fitting noise. shift <- ifelse( brs_genes$up_in[match(genes, brs_genes$current_symbol)] == "RAS", 1, -1 ) expr <- matrix(rnorm(length(genes) * length(samples)), nrow = length(genes), dimnames = list(genes, samples) ) expr <- expr + outer(shift, ifelse(group == "BRAF_V600E", -0.5, 0.5)) labels <- setNames(group[1:16], samples[1:16]) fit <- brs_fit(expr, labels, genes = genes) predict(fit, expr[, 17:20])set.seed(1) genes <- na.omit(brs_genes$current_symbol)[1:10] samples <- paste0("S", 1:20) group <- rep(c("BRAF_V600E", "RAS"), each = 10) # Give the two groups the difference the signature is meant to detect, so # the example shows the classifier working rather than fitting noise. shift <- ifelse( brs_genes$up_in[match(genes, brs_genes$current_symbol)] == "RAS", 1, -1 ) expr <- matrix(rnorm(length(genes) * length(samples)), nrow = length(genes), dimnames = list(genes, samples) ) expr <- expr + outer(shift, ifelse(group == "BRAF_V600E", -0.5, 0.5)) labels <- setNames(group[1:16], samples[1:16]) fit <- brs_fit(expr, labels, genes = genes) predict(fit, expr[, 17:20])
Compares predict.brs_fit() output against a reference label vector.
validate_brs(predictions, labels, fit = NULL)validate_brs(predictions, labels, fit = NULL)
predictions |
A data frame as returned by |
labels |
A named character vector, as in |
fit |
Optionally the |
A list with n, n_concordant, pct_concordant,
n_resubstituted (samples that also defined the centroids, NA if
fit was not given) and the confusion table (published vs.
predicted). The table always carries both classes on both margins, even
when only one of them occurs, so that indexing it by name is safe.
Samples whose label names neither reference group are dropped with a
warning and are not counted in n.
Passing the same labels that were used to fit the centroids measures how
well a nearest-centroid rule reproduces the labels that defined those
centroids. That statistic is strongly optimistic: on 70 genes and 391
samples split 272/119, pure noise with no signal at all still resubstitutes
at about 68%. Pass fit and this function will tell you how much of the
comparison is resubstitution, and warn when all of it is.
For a real check, compare against labels that did not enter the fit.
For TCGA-THCA, fit on the mutation groups and validate against the
published BRAF_RAF_class and BRAF_RAF_score.
set.seed(1) genes <- na.omit(brs_genes$current_symbol)[1:10] samples <- paste0("S", 1:20) group <- rep(c("BRAF_V600E", "RAS"), each = 10) # Give the two groups the difference the signature is meant to detect, so # the example shows the classifier working rather than fitting noise. shift <- ifelse( brs_genes$up_in[match(genes, brs_genes$current_symbol)] == "RAS", 1, -1 ) expr <- matrix(rnorm(length(genes) * length(samples)), nrow = length(genes), dimnames = list(genes, samples) ) expr <- expr + outer(shift, ifelse(group == "BRAF_V600E", -0.5, 0.5)) labels <- setNames(group[1:16], samples[1:16]) # Fit on 12 samples and compare against all 20, so that 8 of the # samples scored did not help define the centroids. Passing `fit` makes # validate_brs() report how much of the comparison is resubstitution. in_fit <- samples[c(1:6, 11:16)] fit <- brs_fit(expr, labels[in_fit], genes = genes) preds <- predict(fit, expr) validate_brs(preds, setNames(group, samples), fit = fit)set.seed(1) genes <- na.omit(brs_genes$current_symbol)[1:10] samples <- paste0("S", 1:20) group <- rep(c("BRAF_V600E", "RAS"), each = 10) # Give the two groups the difference the signature is meant to detect, so # the example shows the classifier working rather than fitting noise. shift <- ifelse( brs_genes$up_in[match(genes, brs_genes$current_symbol)] == "RAS", 1, -1 ) expr <- matrix(rnorm(length(genes) * length(samples)), nrow = length(genes), dimnames = list(genes, samples) ) expr <- expr + outer(shift, ifelse(group == "BRAF_V600E", -0.5, 0.5)) labels <- setNames(group[1:16], samples[1:16]) # Fit on 12 samples and compare against all 20, so that 8 of the # samples scored did not help define the centroids. Passing `fit` makes # validate_brs() report how much of the comparison is resubstitution. in_fit <- samples[c(1:6, 11:16)] fit <- brs_fit(expr, labels[in_fit], genes = genes) preds <- predict(fit, expr) validate_brs(preds, setNames(group, samples), fit = fit)