This document gives an introduction to and overview of inferring the clone identity of cells using the cardelino package using a given clonal structure.
cardelino can use variant information extracted from single-cell RNA-seq reads to probabilistically assign single-cell transcriptomes to individual clones.
Briefly, cardelino is based on a Bayesian mixture model with a beta-binomial error model to account for sequencing errors as well as a gene-specific model for allelic imbalance between haplotypes and associated bias in variant detection. Bayesian inference allows the model to account for uncertainty in model parameters and cell assignments.
We assume that clones are tagged by somatic mutations, and that these mutations are known (e.g. from exome sequencing or equivalent). Given a set of known mutations, these sites can be interrogated in scRNA-seq reads to obtain evidence for the presence or absence of each mutation in each cell. As input, the model requires the count of reads supporting the alternative (mutant) allele at each mutation site, the total number of reads overlapping the mutation site (“coverage”).
Typically, coverage of somatic mutations in scRNA-seq data is very sparse (most mutation sites in a given cell have no read coverage), but the cardelino model accounts for this sparsity and aggregates information across all available mutation sites to infer clonal identity.
In many clone ID scenarios, a clonal tree is known. That is, we have been able to infer the clones present in the sampled cell population, for example using bulk or single-cell DNA-seq data, and we know which mutations are expected to be present in which clones.
To infer the clonal identity of cells when a clonal tree is provided, cardelino requires the following input data:
The configuration matrix, Config, can be provided by other tools used to infer the clonal structure of the cell population. For example, the package Canopy can be used to infer a clonal tree from DNA-seq data and the “Z” element of its output is the configuration matrix.
Here, we demonstrate the use of cardelino to assign 77 cells to clones identified with Canopy using 112 somatic mutations.
We load the package and the example clone ID dataset distributed with
the package in VCF (variant call format)
format, which is mostly used for storing genotype data. Here, the
cellSNP.cells.vcf.gz
is computed by using cellsnp-lite on a list
pre-identified somatic variants from bulk WES.
There are many possible ways to extract the data required by
cardelino
from a VCF file, here we show just one approach
using convenience functions in cardelino
:
vcf_file <- system.file("extdata", "cellSNP.cells.vcf.gz",
package = "cardelino")
input_data <- load_cellSNP_vcf(vcf_file)
## Scanning file to determine attributes.
## File attributes:
## meta lines: 37
## header_line: 38
## variant count: 112
## column count: 86
## Meta line 37 read in.
## All meta lines processed.
## gt matrix initialized.
## Character matrix gt created.
## Character matrix gt rows: 112
## Character matrix gt cols: 86
## skip: 0
## nrows: 112
## row_num: 0
## Processed variant: 112
## All variants processed
## [1] "112 out of 112 SNPs passed."
Alternatively you can load the A
and D
matrices yourself and combine them into a list, for example
input_data = list('A' = A, 'D' = D)
.
We can visualize the allele frequency of the mutation allele. As expected, the majority of entries are missing (in grey) due to the high sparsity in scRNA-seq data. For the same reason, even for the non-missing entries, the estimate of allele frequency is of high uncertainty. For this reason, it is crucial to probabilistic clustering with accounting the uncertainty, ideally with guide clonal tree structure from external data.
AF <- as.matrix(input_data$A / input_data$D)
p = pheatmap::pheatmap(AF, cluster_rows=FALSE, cluster_cols=FALSE,
show_rownames = TRUE, show_colnames = TRUE,
labels_row='77 cells', labels_col='112 SNVs',
angle_col=0, angle_row=0)
Next, we will load the Canopy tree results for the same individual. The clonal tree inferred by Canopy for this donor consists of three clones, including a “base” clone (“clone1”) that has no subclonal somatic mutations present.
canopy_res <- readRDS(system.file("extdata", "canopy_results.coveraged.rds",
package = "cardelino"))
Config <- canopy_res$tree$Z
Be careful to ensure that the same variant IDs are used in both data sources.
We can visualize the clonal tree structure obtained from
Canopy
:
The included dataset contains the A and D matrices, so combined with
the Canopy tree object provided, we have the necessary input to
probabilistically assign cells to clones. Note,
min_iter = 800, max_iter = 1200
is used only for quick
illustration. Please remove them for the default values or set higher
number of iterations to ensure convergence of the Gibbs sampling.
Convergence is checked automatically in clone_id()
, using
the Geweke z-statistic.
set.seed(7)
assignments <- clone_id(input_data$A, input_data$D, Config = Config,
min_iter = 800, max_iter = 1200)
## 100% converged.
## [1] "Converged in 800 iterations."
## DIC: 1384.96 D_mean: 1230.68 D_post: 1203.9 logLik_var: 45.27
## [1] "theta0" "theta1" "theta0_all" "theta1_all"
## [5] "element" "logLik" "prob_all" "prob"
## [9] "prob_variant" "relax_rate" "Config_prob" "Config_all"
## [13] "relax_rate_all" "DIC" "n_chain"
We can visualise the cell-clone assignment probabilities as a heatmap.
We recommend assigning a cell to the highest-probability clone if the
highest posterior probability is greater than 0.5 and leaving cells
“unassigned” if they do not reach this threshold. The
assign_cells_to_clones
function conveniently assigns cells
to clones based on a threshold and returns a data.frame with the
cell-clone assignments.
## cell clone prob_max
## 1 ERR2806034 clone2 1.0000000
## 2 ERR2806035 clone1 0.9999971
## 3 ERR2806036 clone1 0.9998761
## 4 ERR2806037 clone1 0.9996611
## 5 ERR2806038 clone1 0.9999999
## 6 ERR2806039 clone2 1.0000000
##
## clone1 clone2 clone3 unassigned
## 44 24 7 2
Also, Cardelino will update the guide clonal tree Config matrix (as a prior) and return a posterior estimate. In the figure below, negative value means the probability of a certain variant presents in a certain clone is reduced in posterior compared to prior (i.e., the input Config). Vice verse for the positive values.
heat_matrix(t(assignments$Config_prob - Config)) +
scale_fill_gradient2() +
ggtitle('Changes of clonal Config') +
labs(x='Clones', y='112 SNVs') +
theme(axis.text.y = element_blank(), legend.position = "right")
Finally, we can visualize the results cell assignment and updated mutations clonal configuration at the raw allele frequency matrix:
As shown above, the Config can be updated by the observations from
scRNA-seq data. A step further is to not including the Config entirely.
This can be possible, as there could be no clonal tree available. In
this case, you can use cardelino in its de-novo by set
Config=NULL
and set a number for n_clones
.
Note, by default we will keep the first clone as base clone, i.e., no
mutations. You can turn it off by set
keep_base_clone=FALSE
.
{denovo-cell-assign} assignments <- clone_id(input_data$A, input_data$D, Config=NULL, n_clone = 3)
As an further illustration, we will show how cardelino can be used to
infer the clonal structure from mitochondrial variations, that called
from MQuad.
Again, we have included the MQuad output in .mtx
format in
the cardelino package. Note, this mitochondrial data is from the same
SMART-seq data set above.
First, let’s import these two AD and DP matrices and together with their variant names.
AD_file <- system.file("extdata", "passed_ad.mtx", package = "cardelino")
DP_file <- system.file("extdata", "passed_dp.mtx", package = "cardelino")
id_file <- system.file("extdata", "passed_variant_names.txt",
package = "cardelino")
AD <- Matrix::readMM(AD_file)
DP <- Matrix::readMM(DP_file)
var_ids <- read.table(id_file, )
rownames(AD) <- rownames(DP) <- var_ids[, 1]
colnames(AD) <- colnames(DP) <- paste0('Cell', seq(ncol(DP)))
Same as above, AD and DP are matrices with variants as rows and cells as column. In this case we have 25 variants (rows) and 77 cells (columns). As expected, the coverage of mtDNA is a lot higher than the nuclear genome above (much fewer missing values).
Now, we can run cardelino on the mitochondrial variations. Note, as there is no prior clonal tree, the model is easier to return a local optima. Generally, we recommend running it multiple time (with different random seed or initializations) and pick the one with highest DIC.
## Config is NULL: de-novo mode is in use.
## 100% converged.
## [1] "Converged in 5000 iterations."
## DIC: NaN D_mean: Inf D_post: Inf logLik_var: NaN
Then visualise allele frequency along with the clustering of cells and variants:
## R version 4.4.2 (2024-10-31)
## Platform: x86_64-pc-linux-gnu
## Running under: Ubuntu 24.04.1 LTS
##
## Matrix products: default
## BLAS: /usr/lib/x86_64-linux-gnu/openblas-pthread/libblas.so.3
## LAPACK: /usr/lib/x86_64-linux-gnu/openblas-pthread/libopenblasp-r0.3.26.so; LAPACK version 3.12.0
##
## locale:
## [1] LC_CTYPE=en_US.UTF-8 LC_NUMERIC=C
## [3] LC_TIME=en_US.UTF-8 LC_COLLATE=C
## [5] LC_MONETARY=en_US.UTF-8 LC_MESSAGES=en_US.UTF-8
## [7] LC_PAPER=en_US.UTF-8 LC_NAME=C
## [9] LC_ADDRESS=C LC_TELEPHONE=C
## [11] LC_MEASUREMENT=en_US.UTF-8 LC_IDENTIFICATION=C
##
## time zone: Etc/UTC
## tzcode source: system (glibc)
##
## attached base packages:
## [1] stats graphics grDevices utils datasets methods base
##
## other attached packages:
## [1] cardelino_1.9.0 ggplot2_3.5.1 knitr_1.49 BiocStyle_2.35.0
##
## loaded via a namespace (and not attached):
## [1] DBI_1.2.3 bitops_1.0-9
## [3] permute_0.9-7 rlang_1.1.4
## [5] magrittr_2.0.3 vcfR_1.15.0
## [7] matrixStats_1.4.1 compiler_4.4.2
## [9] RSQLite_2.3.8 mgcv_1.9-1
## [11] GenomicFeatures_1.59.1 png_0.1-8
## [13] vctrs_0.6.5 combinat_0.0-8
## [15] memuse_4.2-3 pkgconfig_2.0.3
## [17] crayon_1.5.3 fastmap_1.2.0
## [19] XVector_0.47.0 labeling_0.4.3
## [21] utf8_1.2.4 Rsamtools_2.23.0
## [23] rmarkdown_2.29 UCSC.utils_1.3.0
## [25] purrr_1.0.2 bit_4.5.0
## [27] xfun_0.49 zlibbioc_1.52.0
## [29] cachem_1.1.0 aplot_0.2.3
## [31] GenomeInfoDb_1.43.1 jsonlite_1.8.9
## [33] blob_1.2.4 DelayedArray_0.33.2
## [35] BiocParallel_1.41.0 cluster_2.1.6
## [37] parallel_4.4.2 R6_2.5.1
## [39] VariantAnnotation_1.53.0 bslib_0.8.0
## [41] RColorBrewer_1.1-3 rtracklayer_1.67.0
## [43] GenomicRanges_1.59.1 jquerylib_0.1.4
## [45] Rcpp_1.0.13-1 SummarizedExperiment_1.37.0
## [47] IRanges_2.41.1 Matrix_1.7-1
## [49] splines_4.4.2 tidyselect_1.2.1
## [51] abind_1.4-8 yaml_2.3.10
## [53] vegan_2.6-8 codetools_0.2-20
## [55] curl_6.0.1 lattice_0.22-6
## [57] tibble_3.2.1 treeio_1.31.0
## [59] Biobase_2.67.0 withr_3.0.2
## [61] KEGGREST_1.47.0 evaluate_1.0.1
## [63] pinfsc50_1.3.0 gridGraphics_0.5-1
## [65] survival_3.7-0 snpStats_1.57.0
## [67] Biostrings_2.75.1 pillar_1.9.0
## [69] BiocManager_1.30.25 ggtree_3.15.0
## [71] MatrixGenerics_1.19.0 stats4_4.4.2
## [73] ggfun_0.1.7 generics_0.1.3
## [75] RCurl_1.98-1.16 S4Vectors_0.45.2
## [77] munsell_0.5.1 scales_1.3.0
## [79] tidytree_0.4.6 glue_1.8.0
## [81] pheatmap_1.0.12 lazyeval_0.2.2
## [83] maketools_1.3.1 tools_4.4.2
## [85] BiocIO_1.17.1 sys_3.4.3
## [87] BSgenome_1.75.0 GenomicAlignments_1.43.0
## [89] buildtools_1.0.0 fs_1.6.5
## [91] XML_3.99-0.17 grid_4.4.2
## [93] tidyr_1.3.1 ape_5.8
## [95] AnnotationDbi_1.69.0 colorspace_2.1-1
## [97] patchwork_1.3.0 nlme_3.1-166
## [99] GenomeInfoDbData_1.2.13 restfulr_0.0.15
## [101] cli_3.6.3 fansi_1.0.6
## [103] viridisLite_0.4.2 S4Arrays_1.7.1
## [105] dplyr_1.1.4 gtable_0.3.6
## [107] yulab.utils_0.1.8 sass_0.4.9
## [109] digest_0.6.37 BiocGenerics_0.53.3
## [111] ggplotify_0.1.2 SparseArray_1.7.2
## [113] farver_2.1.2 rjson_0.2.23
## [115] memoise_2.0.1 htmltools_0.5.8.1
## [117] lifecycle_1.0.4 httr_1.4.7
## [119] MASS_7.3-61 bit64_4.5.2