CanDI and DESeq2
Let’s say I want to look at changes in RNA expression across some cell lines in CCLE. DESeq2 is my preffered tool for doing differential expression analysis, unforutantely it’s written in R. CanDI makes it easy to format CCLE read counts data into the shape that DESeq2 expects.
import pandas as pd
import anndata as ad
import CanDI.candi as can
from CanDI.pipelines import diffexp
Object Instantiation
For this example I’m going to do differential expression analysis across male and female KRAS mutant cell lines. The cell below uses CanDI to generate the correct CellLineCluster objects for our purpose.
if type(can.data.mutations) != pd.DataFrame:
can.data.load('mutations')
lung = can.Cancer("Lung Cancer", subtype = "NSCLC")
lung = can.CellLineCluster(lung.mutated("KRAS", variant = "Variant_Classification", item = "Missense_Mutation"))
lung_male = can.CellLineCluster(list(lung._info.loc[lung._info.sex == "Male",].index))
lung_female = can.CellLineCluster(list(lung._info.loc[lung._info.sex == "Female"].index))
Data Munging
The follow function takes two objects that we want to compare and automatically generates the counts and coldata matrices that DESeq2 needs to run. It’s typically a good idea to filter our genes/transcripts with consistently low counts prior to running DESeq2. This speeds up analysis and avoids issues related to read count scaling and multiple hypthothesis testing corrections. In this case we don’t care about different splicing of the same genes so I sum counts for duplicate indeces for all samples.
if type(can.data.rnaseq_reads) != pd.DataFrame:
can.data.load('rnaseq_reads')
def make_counts_coldata(obj1, obj2, condition, factor1, factor2):
counts1 = obj1.rnaseq_reads
coldat1 = pd.Series(counts1.shape[1] * [factor1], index = counts1.columns, name = condition)
counts2 = obj2.rnaseq_reads
coldat2 = pd.Series(counts2.shape[1] * [factor2], index = counts2.columns, name = condition)
#Concatenate Column Data
coldat = pd.concat([coldat1, coldat2], axis = 0)
#Concatenate read count data
counts_mat = pd.concat([counts1, counts2], axis = 1)
#Sum duplicate indeces
counts_mat = counts_mat.groupby(counts_mat.index).sum().astype(int)
adata = ad.AnnData(
counts_mat.T,
obs = coldat.to_frame()
)
return adata
adata = make_counts_coldata(lung_male, lung_female, "sex", "male", "female")
Running pyDESeq2
results = diffexp.run_deseq(
adata,
design = 'sex',
tested_level = 'male',
ref_level = 'female'
)
Fitting size factors...
... done in 0.05 seconds.
Fitting dispersions...
... done in 1.39 seconds.
Fitting dispersion trend curve...
... done in 0.58 seconds.
Fitting MAP dispersions...
... done in 1.42 seconds.
Fitting LFCs...
... done in 2.28 seconds.
Replacing 5676 outlier genes.
Fitting dispersions...
... done in 0.28 seconds.
Fitting MAP dispersions...
... done in 0.28 seconds.
Fitting LFCs...
... done in 0.36 seconds.
Running Wald tests...
... done in 1.64 seconds.
Log2 fold change & Wald test p-value: sex male vs female
baseMean log2FoldChange lfcSE stat pvalue padj
gene
A1BG 200.696432 0.027944 0.671884 0.041591 0.966825 0.994527
A1BG-AS1 255.912479 0.202698 0.626175 0.323708 0.746159 0.950009
A1CF 12.057056 -1.367054 0.537503 -2.543342 0.010980 0.236614
A2M 18.685828 0.365012 0.619866 0.588857 0.555957 0.902738
A2M-AS1 49.283425 -0.266289 0.618898 -0.430264 0.667004 0.933027
... ... ... ... ... ... ...
ZYG11AP1 0.038949 0.057154 3.391294 0.016853 0.986554 NaN
ZYG11B 2200.470135 -0.200760 0.214134 -0.937546 0.348478 0.816844
ZYX 11155.922014 0.206356 0.377946 0.545994 0.585070 0.912754
ZZEF1 4400.173132 -0.483351 0.222239 -2.174911 0.029637 0.362384
ZZZ3 3532.326301 -0.212461 0.170959 -1.242756 0.213958 0.722301
[52443 rows x 6 columns]
Analyzing Results
Now we can read the results of the differential expression analysis back into our python enviroment and continue analysis as necessary. Looking at the genes with the lowest adjusted p-value we see that XIST is the most significant differentially expressed genes. XIST is a lncRNA responsible for X inactivation in females so this a good positive control for our analysis.
results.sort_values("padj").head()
| baseMean | log2FoldChange | lfcSE | stat | pvalue | padj | |
|---|---|---|---|---|---|---|
| gene | ||||||
| XIST | 4041.971706 | -7.145007 | 0.710899 | -10.050660 | 9.125401e-24 | 2.692176e-19 |
| RPS4Y1 | 6613.044427 | 8.723193 | 1.031580 | 8.456144 | 2.763634e-17 | 4.076636e-13 |
| CEACAM5 | 10444.137420 | -7.282857 | 0.892029 | -8.164374 | 3.231066e-16 | 3.177430e-12 |
| DDX3Y | 1195.388509 | 7.642864 | 0.983916 | 7.767805 | 7.985781e-15 | 5.889913e-11 |
| GJB1 | 58.891176 | -5.623715 | 0.752347 | -7.474896 | 7.726502e-14 | 4.558945e-10 |