---
title: "Transcriptomic Classification using Mixture of Normal Distributions"
output:
BiocStyle::html_document:
toc: true
bibliography: splitTypeR.bibtex
vignette: >
%\VignetteIndexEntry{Transcriptomic Classification using Mixture of Normal Distributions}
%\VignettePackage{splitTypeR}
%\VignetteEngine{knitr::rmarkdown}
%\VignetteEncoding{UTF-8}
---
```{r style, echo = FALSE, results = 'asis', message = FALSE}
BiocStyle::markdown()
suppressPackageStartupMessages({
library(knitr)
library(SummarizedExperiment)
})
```
**Package**: `r Rpackage("splitTypeR")`
**Authors**: `r packageDescription("splitTypeR")[["Author"]]`
**Version**: `r packageDescription("splitTypeR")$Version`
**Compiled date**: `r Sys.Date()`
**License**: `r packageDescription("splitTypeR")[["License"]]`
# Licensing and citing
This package and the underlying __`r Rpackage("splitTypeR")`__ code are
distributed under the Artistic license 2.0. You are free to use and
redistribute this software.
If you use this package for a publication, we would ask you to cite the
following:
```{r citation, echo=TRUE}
## Citation information for the splitTypeR package
citation("splitTypeR")
```
# Introduction
Cancer classification, using RNA sequencing, is now part of modern precision
oncology. For some cancers, RNA classification provides deep insights into
tumor aggressiveness and growth rates. For example, breast cancer subtypes
(like Luminal-A, Luminal-B, HER2-enriched, or Basal-like) help forecast
patient outcomes [@Cascianelli2020]. The Luminal-A is the most
common molecular subtype of breast cancer, making up to 60% of all breast
cancer cases. It is also associated with good prognosis, while
Luminal-B tumors have a more aggressive phenotype and a worse
prognosis [@Yersal2014]. Once widely accepted by major oncology
organizations, gene signatures for RNA subtyping are used to classify cancer
subtypes, predict patient survival, and guide treatment.
From a research perspective, the classification of new samples using their
transcriptional profiles can be done using different tools. For some cancers,
machine learning models and dedicated software are available such as
the Bioconductor Genefu package for breast cancer molecular
subtyping [@Gendoo2016], and the CMScaller package for subtyping of
colorectal cancer [@Eide2017].
In the absence of dedicated software, unsupervised clustering with a
heatmap visualization to classify transcriptomic samples with a
gene signature is often employed. This method is relatively simple and
offers instant visual support. However, this method has a few drawbacks:
* _Lack of statistical metrics_: It doesn't
provide a statistical probability score for a sample's class assignment; it
mainly acts as a visual summary.
* _Metric sensitivity_: Changing the clustering metric (e.g., from "Euclidean" to
"Pearson" distance) can alter the resulting clusters.
* _Outlier distortion_: Extreme outliers can squash the color scale, and
artificially create skewed subsets.
The __`r Rpackage("splitTypeR")`__ package resolves these issues by providing
an automated statistical framework to classify heterogeneous biological
samples based on gene signature lists, effectively isolating the
signature-positive samples.
This classification method is designed for bulk transcriptomic datasets.
# Installation
As with any R package, the __`r Rpackage("splitTypeR")`__ package should first
be loaded with the following command:
```{r loadingPackage, warning=FALSE, message=FALSE}
library(splitTypeR)
```
# Quick start
The __`r Rpackage("splitTypeR")`__ package includes a small demonstration
dataset: two gene signatures
(`signaturesDemo`) and a matrix of normalized expected counts for 30
patients (`expNormalCountsDemo`). The whole subtyping method can be run
on it in a few lines:
```{r quickStart, echo=TRUE}
## Load the demo signatures and the demo normalized expected counts
data(signaturesDemo)
data(expNormalCountsDemo)
## Fix seed for reproducible results
set.seed(121)
## Run the subtyping method
## The normalized expected counts used as input are log2-transformed
quickResult <- runSubtypingBimodal(geneLists=signaturesDemo,
expectedCountsMatrixLog=log2(expNormalCountsDemo + 1),
permRatio=0.75, permNbr=20, upscaleNbr=10)
## Display a summary of the results
quickResult
```
Each step of the method is detailed in the next section, followed by a
complete demonstration on a published dataset.
# Method
The following figure represents the statistical method employed to turn a noisy,
single-number enrichment score into a confident binary classification of
each sample. In summary, the method estimates the uncertainty around each
enrichment score, models the cohort as a mixture of two biological states, and
then statistically tests each sample against that mixture.
```{r pipelineSchema, echo=FALSE, fig.align='center', out.width="100%", fig.cap="General workflow."}
knitr::include_graphics("enrichment_pipeline_300dpi_crop2.jpg")
```
The statistical method is split into 5 steps. Each of those steps is
performed for one specific signature at a time. A signature is typically a
predefined set of features (transcriptomic expression in this situation) that
is associated with some biological state, pathway, or phenotype.
## Step 1. Enrichment score
The enrichment scores are first computed for every single sample
with the Bioconductor `r BiocStyle::Biocpkg("GSVA")`
package [@Hanzelmann2013]. Those scores represent the real score for each
sample. Later steps treat this real score as the anchor point around which
uncertainty gets modeled.
## Step 2. Permutation
Random subsets of samples, of fixed size, from the full dataset are
repeatedly drawn. For each of these resampled subsets, the enrichment score
for the samples present is recomputed.
This step enables a per-sample uncertainty estimation (standard deviation).
## Step 3. Upscaling
For each sample, a fixed number of simulated enrichment scores are drawn
from a normal distribution centered on the real score, using the standard
deviation estimated from permutations.
This step enables the upscaling of the total number of values that will
be used in the next step.
## Step 4. Mixture model
The synthetic values across all samples are pooled to fit two overlapping
normal distributions to them. The two normal distributions are extracted using
a mixture model approach implemented in the `r BiocStyle::CRANpkg("mixtools")`
package [@JSSv032i06]. The assumption is that the whole cohort is a
blend of two underlying populations (a "low-mean" population and a
"high-mean" population).
## Step 5. Classification
Each sample is classified with a one-sided hypothesis test based on the
"low-mean" distribution of the mixture model (from Step 4):
* **Null hypothesis (H₀)**: the sample belongs to the "low-mean" distribution
* **Alternative hypothesis (H₁)**: the sample belongs to the "high-mean" distribution
The 95th percentile of the "low-mean" distribution is used as the
classification threshold. Each sample's real enrichment score (from Step 1)
is compared to this threshold. A score above the threshold has less than a
5% probability of being produced by the "low-mean" distribution (one-sided
p-value < 0.05). In that case, H₀ is rejected.
```{r step5Schematic, echo=FALSE, message=FALSE, fig.width=7, fig.height=4, fig.align='center', fig.cap="Classification of the samples (schematic). The curve represents the \"low-mean\" distribution of the mixture model. The dashed line is the classification threshold (95th percentile) and the shaded area represents the upper 5% of the distribution. Sample A, with a score below the threshold, remains unclassified while sample B, with a score above the threshold, is assigned to the signature."}
library(ggplot2)
## Illustrative "low-mean" distribution (values chosen for the schematic)
lowMean <- -0.4
lowSD <- 0.2
threshold <- qnorm(p=0.95, mean=lowMean, sd=lowSD)
curve <- data.frame(score=seq(-1, 0.6, length.out=500))
curve$density <- dnorm(curve$score, mean=lowMean, sd=lowSD)
## Two example samples, one on each side of the threshold
examples <- data.frame(score=c(-0.3, 0.25),
label=c("Sample A\nunclassified", "Sample B\nassigned to\nthe signature"),
hjust=c(0.5, 0), nudgeX=c(0, -0.02))
ggplot(curve, aes(x=score, y=density)) +
geom_area(data=curve[curve$score >= threshold, ], fill="firebrick",
alpha=0.4) +
geom_line(linewidth=1) +
geom_vline(xintercept=threshold, linetype="dashed", color="gray30") +
annotate("text", x=threshold + 0.02, y=max(curve$density) * 1.05,
label="Threshold\n(95th percentile)", hjust=0, vjust=1, size=3.5) +
annotate("text", x=threshold + 0.035, y=0.12, label="5%",
color="firebrick4", size=3.5, fontface="bold") +
geom_point(data=examples, aes(x=score, y=0), size=3.5, shape=17) +
geom_text(data=examples, aes(x=score + nudgeX, y=0.1, label=label,
hjust=hjust), vjust=0, size=3.2, lineheight=0.9) +
labs(x="Enrichment score", y="Density",
title="\"Low-mean\" distribution of the mixture model") +
theme_bw()
```
When H₀ is rejected (p-value < 0.05), the sample is assigned to the tested
signature. Otherwise, the sample is labeled as "unclassified" for that
signature.
Since each signature is tested independently, a sample can be assigned
to one signature, to several signatures, or to none of them.
# Available signatures
The __`r Rpackage("splitTypeR")`__ package provides transcriptomic signatures.
The _getGeneSignaturesNames()_ function lists the names of all the
available signatures.
```{r signaturesNames, echo=TRUE}
## List the names of the available signatures
getGeneSignaturesNames()
```
The _getGenesInSignature()_ function returns the genes of one signature,
while the _getGeneSignatures()_ function returns a named list containing one
or more signatures. When no name is given, _getGeneSignatures()_ returns all
the available signatures.
```{r signaturesContent, echo=TRUE}
## Get the genes of the basal-like signature
getGenesInSignature(name="Tiriac_PDAC_PDO_Basal_like")
## Get all the available signatures and count the genes in each one
allSignatures <- getGeneSignatures()
lengths(allSignatures)
```
The current signatures are:
* **Tiriac_PDAC_PDO_Basal_like**: the basal-like signature
(26 genes) defined for pancreatic ductal adenocarcinoma (PDAC)
patient-derived organoids (PDOs) in Tiriac et al. [@Tiriac2018].
* **Tiriac_PDAC_PDO_Classical**: the classical signature
(62 genes) defined for PDAC PDOs in Tiriac et al. [@Tiriac2018].
* **Moffitt_Basal_like**: the original basal-like signature
(25 genes) defined for PDAC in Moffitt et al. [@Moffitt2015].
* **Moffitt_Classical**: the original classical signature
(25 genes) defined for PDAC in Moffitt et al. [@Moffitt2015].
* **Miyabayashi_Fast**: the fast signature (231 genes), updated from the
Ensembl IDs (GENCODE v48/org.Hs.eg.db), defined
for PDAC xenografts in Miyabayashi et al. [@Miyabayashi2020].
* **Miyabayashi_Slow**: the slow signature (333 genes), updated from the
Ensembl IDs (GENCODE v48/org.Hs.eg.db), defined
for PDAC xenografts in Miyabayashi et al. [@Miyabayashi2020].
## Using your own signatures
The method is not limited to the signatures included in the
__`r Rpackage("splitTypeR")`__
package. Any named `list` of at least two gene vectors can be passed to the
_runSubtypingBimodal()_ function. The names of the list are used as the
signature names in the results. The gene identifiers must use the same
format as the row names of the expression matrix (e.g., gene names, Ensembl
IDs). Genes absent from the expression matrix are ignored. It is recommended
to use signatures with 10 genes or more to ensure statistical stability and
reliability of the enrichment scores.
```{r ownSignatures, echo=TRUE, eval=FALSE}
## Example of custom signatures
mySignatures <- list(
SignatureA=c("GENE1", "GENE2", "GENE3", "GENE4", "GENE5", "GENE6",
"GENE7", "GENE8", "GENE9", "GENE10", "GENE11"),
SignatureB=c("GENE4", "GENE9", "GENE10", "GENE12", "GENE13", "GENE15",
"GENE16", "GENE17", "GENE18", "GENE19", "GENE20", "GENE21"))
```
# Demonstration
For this demonstration, we are going to use the published RNA-seq dataset of
pancreatic ductal adenocarcinoma (PDAC) patient-derived organoids (PDOs)
published in Tiriac et al. [@Tiriac2018]. The RNA-seq has been aligned with
GENCODE v48 on GRCh38.p14 genome with STAR v2.7.9a [@Dobin2013]. Only one
PDO per patient was retained. The expected counts were processed with
`r BiocStyle::Biocpkg("DESeq2")` v1.50.2 [@Love2014] on genes expressed in at
least 20% of the PDOs. The normalized expression is available in a
_SummarizedExperiment_ object.
In PDAC, the basal-like and classical subtypes, as defined in Moffitt et
al. [@Moffitt2015], are the two consistently employed molecular subtypes. The
classical signature is associated with better prognosis and better response to
standard chemotherapy, while the basal-like subtype is associated
with worse overall prognosis [@Moffitt2015; @OKane2020]. These signatures were
later redefined for patient-derived organoids [@Tiriac2018].
## Prepare the signatures
The basal-like and classical gene signatures used in the demonstration are
the ones defined for PDOs in Tiriac et al. [@Tiriac2018].
```{r geneSignatures, echo=TRUE}
## Required package
library(splitTypeR)
## Prepare the PDOs basal-like and classical signatures
signaturesList <- list()
signaturesList[["Basal-like"]] <-
getGenesInSignature("Tiriac_PDAC_PDO_Basal_like")
signaturesList[["Classical"]] <-
getGenesInSignature("Tiriac_PDAC_PDO_Classical")
```
## Prepare the log2-transformed normalized expected counts
A matrix of normalized expected counts is required. The rows are the genes,
while the patients (samples) are the columns.
The row annotation must correspond to the signature format. As an example, if
the signatures are composed of gene names, the row annotation for the
normalized expected counts must also be gene names.
For GSVA analysis, the normalized expected counts should be log-transformed to
enable the use of the Gaussian distribution function. The current
behavior of GSVA is to automatically choose one of the available distribution
functions.
```{r expCount, echo=TRUE}
## Required package
library(SummarizedExperiment)
## Load the SummarizedExperiment object
path <- system.file("extdata", "Tiriac_NormExpCounts.RDS",
package="splitTypeR")
se <- readRDS(path)
## Extract the normalized counts and assign the gene names as row names
## The row names should correspond to the information present in the
## gene signatures. In this case, the gene names are used.
normCounts <- assays(se)[["normalizedCounts"]]
rownames(normCounts) <- rowRanges(se)$gene_name
## Log2-transform the data, adding a small pseudo-count to
## prevent log2(0) errors
log2normCounts <- log2(normCounts + 1)
```
## Run the subtyping method
The _runSubtypingBimodal_ function runs all the steps. Three parameters
control the permutation and upscaling steps:
| Parameter | Description | Default | Trade-off |
|:----------|:------------|:-------:|:----------|
| `permRatio` | Proportion of the samples randomly selected in each permutation (Step 2). Must be between 0 and 1, and must not select all the samples. | 0.75 | A lower value creates more variation between permutations, but each GSVA score is computed on fewer samples. |
| `permNbr` | Number of permutations (Step 2). Must be 5 or higher. | 20 | More permutations give a more stable standard deviation for each sample, but take longer to run since GSVA is run once per permutation. |
| `upscaleNbr` | Number of values drawn from the normal distribution of each sample (Step 3). Must be 2 or higher. | 10 | More values give more data to fit the mixture model (Step 4), with little impact on the run time. |
Each sample is present, on average, in `permRatio` × `permNbr` permutations
(15 with the default values), which is the number of values used to estimate
its standard deviation.
```{r subtyping, echo=TRUE}
## Fix seed for reproducible results
set.seed(81)
## Run the subtyping method using the log2-transformed data
result <- runSubtypingBimodal(geneLists=signaturesList,
expectedCountsMatrixLog=log2normCounts,
permRatio=0.75,
permNbr=20,
upscaleNbr=12)
## Summary of the classification results
result
```
The resulting object contains information from each step of the process.
The information can be accessed using the appropriate getter function.
```{r signaturesGetter, echo=TRUE}
## Get the list of genes in the classical signature
signatures(result)[["Classical"]]
```
```{r gsvaResultsGetter, echo=TRUE}
## Get the GSVA scores for all samples in each signature
gsvaResults(result)
```
```{r permutationGetter, echo=TRUE}
## Get the GSVA scores for the first 5 samples in the first 6 permutations
## (columns) done for the classical signature
permutations(result)[['Classical']][seq_len(5), seq_len(6)]
```
```{r standardDeviationGetter, echo=TRUE}
## Get the standard deviation of the GSVA scores for all samples for the
## classical signature
## The standard deviation is inferred from the permutation step
standardDeviation(result)$Classical
```
```{r upscalingGetter, echo=TRUE}
## Get the first 6 upscaling GSVA scores (columns) for the first 5 samples
## for the classical signature
upscaling(result)[['Classical']][seq_len(5), seq_len(6)]
```
```{r modelGetter, echo=TRUE}
## Get the mixture model for the classical signature
## This is a mixEM object
classicalModel <- model(result)[['Classical']]
## Get the two distribution means
classicalModel$mu
## Get the two distribution standard deviations
classicalModel$sigma
## Get the mixing proportions (the fraction of the values assigned to each
## distribution)
classicalModel$lambda
```
```{r thresholdGetter, echo=TRUE}
## Get the classification threshold of each signature
## The threshold is the 95th percentile of the "low-mean" distribution
threshold(result)
```
```{r classificationGetter, echo=TRUE}
## Get the final classification of the first samples
## for the classical signature
head(classification(result)[['Classical']])
```
## Visual representation of the results at the permutation step
The _plotPermutationSamplesDistribution()_ function enables the visualization
of the GSVA results obtained during the permutation step for a specific
signature and specific samples.
```{r graphPermutation, echo=TRUE}
## The GSVA results obtained for three samples during the permutation step
## for the classical signature
## The seed is fixed to ensure reproducibility
plotPermutationSamplesDistribution(x=result, signature="Classical",
pointColor="darkorange", alpha=1, samples=c("hF23", "hT82", "hT89"),
seed=121)
```
## Visual representation of the results at the upscaling step
From the permutation step, the standard deviation is calculated for each
sample. The upscaling is done with the real GSVA score as mean and the
calculated standard deviation.
The _plotPermutationSamplesNormal()_ function shows the GSVA scores sampled
during the upscaling step. The mean is shown as a vertical dotted line, while
the normal distribution is shown as a dashed line. The sampled GSVA scores
are shown as dots in this graph.
```{r graphUp, echo=TRUE}
## Fix seed for reproducibility
set.seed(121)
## The GSVA scores sampled for three samples during the upscaling step
## for the classical signature
plotPermutationSamplesNormal(result, signature="Classical",
samples=c("hF23", "hT82", "hT89"), pointColor="darkviolet")
```
## Visual representation of the results at the mixture model step
The _plotDensityWithUpscalingSamplesBimodal()_ function enables the
visualization of the two normal distributions obtained for a specific
signature. In this graph, the GSVA values are those generated during the
upscaling step.
```{r graphDensity, echo=TRUE}
plotDensityWithUpscalingSamplesBimodal(breaks = 26L,
signature="Basal-like", result)
plotDensityWithUpscalingSamplesBimodal(breaks = 26L,
signature="Classical", result)
```
## Classification of the samples
Let's compare the classification obtained here with the one from
Tiriac et al. [@Tiriac2018].
```{r classificationTiriac, echo=TRUE}
## Assignments from Tiriac et al.
pathAssign <- system.file("extdata", "Moffitt_Tiriac_2018_Assignation.txt",
package="splitTypeR")
tiriacAssign <- read.table(pathAssign, sep="\t", header=TRUE)
## Assignments from the mixture model are extracted from the result
## By default, the samples are considered unclassified
## The samples are classified only if they are assigned to one of the signatures
## When assigned to both signatures, the samples remain unclassified
assignation <- data.frame(
Samples=rownames(classification(result)[['Classical']]),
newAssignation="unclassified")
assignation$Classical <- classification(result)[['Classical']]$classification
assignation$Basal <- classification(result)[['Basal-like']]$classification
assignation$newAssignation[which(assignation$Classical == "Classical" &
assignation$Basal == "unclassified")] <- "Classical"
assignation$newAssignation[which(assignation$Basal == "Basal-like" &
assignation$Classical == "unclassified")] <- "Basal-like"
## Merge the current assignments with the Tiriac et al. assignments into
## one data frame
assignation <- merge(assignation[, c("Samples", "newAssignation")],
tiriacAssign, by.x="Samples",
by.y="Line", all.y=FALSE, all.x=FALSE)
table(assignation$newAssignation, assignation$Moffitt)
```
About
`r round(sum(assignation$newAssignation == assignation$Moffitt)/nrow(assignation)*100)`%
of
the samples (`r sum(assignation$newAssignation == assignation$Moffitt)` out of
`r nrow(assignation)`) are assigned the same subtype.
The following graph shows the GSVA scores of each sample for both
signatures. The dashed lines represent the classification threshold of each
signature (the 95th percentile of the "low-mean" distribution). A sample
located to the right of the vertical line is assigned to the classical
signature, while a sample located above the horizontal line is assigned to
the basal-like signature. The samples for which the classification differs
from Tiriac et al. [@Tiriac2018] are labeled.
```{r compared, echo=TRUE, fig.width=7, fig.height=5.5}
## Required package
library(ggplot2)
## Get the GSVA score of each sample for both signatures
gsva <- data.frame(
Samples=rownames(classification(result)[['Classical']]),
Classical=classification(result)[['Classical']]$GSVA_score,
Basal=classification(result)[['Basal-like']]$GSVA_score)
## Add the assignments and identify the samples with a different
## classification than Tiriac et al.
gsva <- merge(gsva, assignation, by="Samples")
gsva$Agreement <- ifelse(gsva$newAssignation == gsva$Moffitt,
"Same classification", "Different classification")
## Classification threshold of each signature
thresholdClassical <- threshold(result)[['Classical']]
thresholdBasal <- threshold(result)[['Basal-like']]
## Scatter plot of the GSVA scores with the classification thresholds
## The vertical dashed line is the classical threshold and the horizontal
## dashed line is the basal-like threshold
ggplot(gsva, aes(x=Classical, y=Basal)) +
geom_vline(xintercept=thresholdClassical, linetype="dashed",
color="gray40") +
geom_hline(yintercept=thresholdBasal, linetype="dashed",
color="gray40") +
geom_point(aes(color=Agreement, shape=Moffitt), size=3) +
geom_text(data=gsva[gsva$Agreement == "Different classification", ],
aes(label=Samples), vjust=-1, size=3.5) +
scale_color_manual(values=c("Same classification"="gray60",
"Different classification"="firebrick")) +
labs(x="GSVA score - Classical signature",
y="GSVA score - Basal-like signature",
color="Compared to Tiriac et al.", shape="Tiriac et al.") +
theme_bw()
```
Most samples that are not assigned to the same subtype have intermediate
scores and are located where the classical and basal-like samples meet.
Sample hF50 is located in the region where both signatures are assigned and
therefore remains unclassified.
# Acknowledgment
D.A. Tuveson is a distinguished scholar of the Lustgarten Foundation and
Director of the Lustgarten Foundation-designated Laboratory of Pancreatic
Cancer Research. This project is supported by the Lustgarten Foundation.
Claude Sonnet 5 was used for improving the workflow figure. It was also used
for proofreading and copyediting sections of this text.
Claude Opus 5.5, through Claude Code, was used to assist with code
maintenance, including renaming the demonstration signatures, updating the
associated documentation and unit tests, removing unused internal data, and
adding the Quick start section to this vignette. All changes were reviewed
and validated by the package maintainer.
# Session info
Here is the output of `sessionInfo()` on the system on which this document was
compiled:
```{r sessionInfo, echo=FALSE}
sessionInfo()
```