A major challenge for single cell RNA sequencing of human tumors is to distinguish cancer cells from stromal cell types, as well as the presence of multiple tumor subclones. CopyKAT(Copynumber Karyotyping of Tumors) is a computational tool using integrative Bayesian approaches to identify genome-wide aneuploidy at 5MB resolution in single cells to separate tumor cells from normal cells, and tumor subclones using high-throughput sc-RNAseq data. The underlying logic of calculating DNA copy numbers from RNAseq data is that gene expression levels of many adjacent genes can be dosed by genomic DNA copy numbers in that region. CopyKAT estimated copy numbers can achieve a high concordance (80%) with the actual DNA copy numbers obtained by whole genome DNA sequencing. The rationale for prediction tumor/normal cell states is that aneuploidy is common in human cancers (90%). Cells with extensive genome-wide copy number aberrations (aneuploidy) are considered as tumor cells, wherease stromal normal cells and immune cells often have 2N diploid or near-diploid copy number profiles. In this vignette, I will take you go through the process of calculating single cell copy numbers, predicting tumor and normal cells, and inferring tumor cell subpopulations from single cells RNAseq data using R package {copykat}.
knitr::opts_chunk$set( collapse = TRUE, comment = "#>" )
Installing copykat from GitHub
library(devtools) install_github("navinlabcode/copykat")
An example raw UMI matrix from a breast tumor sequenced by 10X 3'RNAseq protocol is included with this package named exp.rawdata.
To test the package, simply issue this line of code in R/Rstudio:
copykat.test <- copykat(rawmat=exp.rawdata, sam.name="test")
The only one direct input that you need to prepare to run copykat is the raw gene expression matrix, with gene ids in rows and cell names in columns. The gene ids can be gene symbol or ensemble id. The matrix values are often the count of unique molecular identifier (UMI) from nowadays high througput single cell RNAseq data. The early generation of scRNAseq data may be summarized as TPM values or total read counts, which should also work. Below I provide an example of generating this UMI count matrix from 10X output.
library(Seurat) raw <- Read10X(data.dir = data.path.to.cellranger.outs) raw <- CreateSeuratObject(counts = raw, project = "copycat.test", min.cells = 0, min.features = 0) exp.rawdata <- as.matrix(raw@assays$RNA@counts)
I could save the matrix for future use.
write.table(exp.rawdata, file="exp.rawdata.txt", sep="\t", quote = FALSE, row.names = TRUE)
In this vignette, I take the example UMI count matrix, exp.rawdata to demonstrate the workflow.
Now I have prepared the only one input, raw UMI count matrix, I am ready to run copykat. The default gene ids in cellranger output is gene symbol, so I put "Symbol" or "S". To filter out cells, I require at least 5 genes in each chromosome to calculate DNA copy numbers. I can tune this down to ngene.chr=1 to keep as many cells as possible, however I think using at least 5 genes to represent one chromosome is not very stringent. To filter out genes, I can tune parameters to keep only genes that are expressed in LOW.DR to UP.DR fractions of cells. I put default LOW.DR=0.05, UP.DR=0.2. I can tune down these values to keep more genes in the analysis. I need to make sure that LOW.DR is smaller than UP.DR though.
I ask copykat to take at least 25 genes per segment. I can play around with other options ranging 15-150 genes per bin. KS.cut is the segmentation parameter, ranging from 0 to 1. Increasing KS.cut decreases sensitivity, ie. less segments/breakpoints.
Here I do parallel computation by setting n.cores = 4. Default value is 1.
I also give a sample name by setting sam.name="test".
One struggling and intesting observation is that none of one clustering method could fit all datasets. In this version, I add a distance parameters for clustering that include "euclidean" distannce and correlational distance, ie. 1-"pearson" and "spearman" similarity. In general, corretional distances tend to favor noisy data, while euclidean distance tends to favor data with larger CN segments.
I add an option to input known normal cell names as a vector object. Default is NULL.
I add a mode for cell line data that has only aneuploid or diploid cells. Setting this cell line mode by cell.line="yes". Default for tissue samples is cell.line="no". This cell line mode uses systhetic baslines from the data variations.
Now I runn the code:
library(copykat) copykat.test <- copykat(rawmat=exp.rawdata, id.type="S", cell.line="no", ngene.chr=5, win.size=25, KS.cut=0.2, sam.name="test", distance="euclidean", n.cores=4)
It might take a while to run a dataset with more than 10,000 single cells. It is suggested to run large dataset in terminal using "Rscript", instead of running copykat in interactive mode in R/Rstudio. I usually run 'Rscript run_copycat.R' in sever and taking either 10X output or raw UMI count matrix as input using the args.
After this step, copykat aumatically save the calculated copy number matrix, the heatmap and tumor/normal prediction results in my working directory. I can also extract them from the object as follows:
pred.test <- data.frame(copykat.test$prediction) CNA.test <- data.frame(copykat.test$CNAmat)
Now let's look at the prediction results. Predicted aneuploid cells are inferred as tumor cells; diploid cells are stromal normal cells.
Please note that filtered cells are not included in the results. I am debatinng whether should I include all cells in the results.
head(pred.test)
The first 3 columns in the CNA matrix are the genomic coordinates. Rows are 220KB bins in genomic orders.
head(CNA.test[ , 1:5])
Copykat also generate a heatmap plot for estimated copy numbers. Rows are single cells; columns are 220kb bins in genomic order.
my_palette <- colorRampPalette(rev(RColorBrewer::brewer.pal(n = 3, name = "RdBu")))(n = 999) chr <- as.numeric(CNA.test$chrom) %% 2+1 rbPal1 <- colorRampPalette(c('black','grey')) CHR <- rbPal1(2)[as.numeric(chr)] chr1 <- cbind(CHR,CHR) rbPal5 <- colorRampPalette(RColorBrewer::brewer.pal(n = 8, name = "Dark2")[2:1]) com.preN <- pred.test$copykat.pred pred <- rbPal5(2)[as.numeric(factor(com.preN))] cells <- rbind(pred,pred) col_breaks = c(seq(-1,-0.4,length=50),seq(-0.4,-0.2,length=150),seq(-0.2,0.2,length=600),seq(0.2,0.4,length=150),seq(0.4, 1,length=50)) heatmap.3(t(CNA.test[,4:ncol(CNA.test)]),dendrogram="r", distfun = function(x) parallelDist::parDist(x,threads =4, method = "euclidean"), hclustfun = function(x) hclust(x, method="ward.D2"), ColSideColors=chr1,RowSideColors=cells,Colv=NA, Rowv=TRUE, notecol="black",col=my_palette,breaks=col_breaks, key=TRUE, keysize=1, density.info="none", trace="none", cexRow=0.1,cexCol=0.1,cex.main=1,cex.lab=0.1, symm=F,symkey=F,symbreaks=T,cex=1, cex.main=4, margins=c(10,10)) legend("topright", paste("pred.",names(table(com.preN)),sep=""), pch=15,col=RColorBrewer::brewer.pal(n = 8, name = "Dark2")[2:1], cex=0.6, bty="n")
I observed both diploid and aneuploid cells. Next step is to extract aneuploid cells that are considered as tumor cells in aneuploid tumors to define two copy number subpopulations of single tumor cells.
tumor.cells <- pred.test$cell.names[which(pred.test$copykat.pred=="aneuploid")] tumor.mat <- CNA.test[, which(colnames(CNA.test) %in% tumor.cells)] hcc <- hclust(parallelDist::parDist(t(tumor.mat),threads =4, method = "euclidean"), method = "ward.D2") hc.umap <- cutree(hcc,2) rbPal <- colorRampPalette(RColorBrewer::brewer.pal(n = 8, name = "Dark2")[3:4]) subpop <- rbPal(2)[as.numeric(factor(hc.umap))] cells <- rbind(subpop,subpop) chr <- as.numeric(CNA.test$chrom) %% 2+1 rbPal1 <- colorRampPalette(c('black','grey')) CHR <- rbPal1(2)[as.numeric(chr)] chrom <- cbind(CHR,CHR) my_palette <- colorRampPalette(rev(RColorBrewer::brewer.pal(n = 3, name = 'RdBu')))(n = 999) col_breaks = c(seq(-1,-0.4,length=50),seq(-0.4,-0.2,length=150),seq(-0.2,0.2,length=600),seq(0.2,0.4,length=150),seq(0.4, 1,length=50)) heatmap.3(t(tumor.mat),dendrogram="r", distfun = function(x) parallelDist::parDist(x,threads =4, method = "euclidean"), hclustfun = function(x) hclust(x, method="ward.D2"), ColSideColors=chrom,RowSideColors=cells,Colv=NA, Rowv=TRUE, notecol="black",col=my_palette,breaks=col_breaks, key=TRUE, keysize=1, density.info="none", trace="none", cexRow=0.1,cexCol=0.1,cex.main=1,cex.lab=0.1, symm=F,symkey=F,symbreaks=T,cex=1, cex.main=4, margins=c(10,10)) legend("topright", c("c1","c2"), pch=15,col=RColorBrewer::brewer.pal(n = 8, name = "Dark2")[3:4], cex=0.9, bty='n')
Now I have defined two subpopulations with major subclonal differences, I can move forward to compare their gene expression profiles, and evaluate gene dosage effects of subclonal copy number changes.
A final note, I also put some useful annotation data along with copykat: 1) full.anno: the full annotation of 56051 genes (hg38) including absolute positions, chr, start, end, ensemble id, gene symbol and G band. 2) DNA.hg20: the coordinates in hg38 of the 220kb variable bins excluding Y chromosome. 3) cyclegenes: that are removed from copykat analysis. 4) exp.rawdata: UMI matrix from a breast tumor.
Thank you.
Enjoy.
2020 New Year Eve
Add the following code to your website.
For more information on customizing the embed code, read Embedding Snippets.