Skip to article frontmatterSkip to article content
Site not loading correctly?

This may be due to an incorrect BASE_URL configuration. See the MyST Documentation for reference.

Bulk RNA-seq

A bulk RNA-seq experiment is a matrix of counts, samples x genes. CytePool tests it with the methods of limma, edgeR and DESeq2, each checked against the R package (Validation). This page runs the three on one simulated experiment whose true changes are known, and compares their calls with the truth. Choosing a method explains how they differ, and the Quick start walks through limma-voom step by step.

The experiment is the simulated bulk dataset (Datasets): three treatments (control, drug A and drug B) with four replicates each, sequenced in two batches; drug A changes 500 of the 10,000 genes and drug B 300, and one sample, drug_b_3, is noisier than the others. python docs/datasets.py prepare simulated_bulk writes it, as a cube with one group, bulk.

CytePool 0.1.dev42+g6991e63f8, numpy 2.5.4
Cube with 12 samples x 1 groups x 10,000 features
  layers: counts (uint32, lazy)
  cells per entry: not recorded
  samples: sample_id, condition, batch
  groups: group_id
  features: feature_id, symbol

The design

The batch enters the model so that its shift of every gene does not count as noise. Both drugs are compared with the control, the reference level.

Design: 12 samples x 4 columns, formula '~ batch + condition'
  columns: (Intercept), batchbatch2, conditiondrug_a, conditiondrug_b
  contrasts: drug_a_vs_control, drug_b_vs_control
Design check: 12 samples, 4 columns, rank 4, 8 residual degrees of freedom; no problems found.

limma-voom, with sample quality weights

limma_voom runs on the cube’s one group: genes are filtered by expression, TMM factors normalize the libraries, voom gives every observation a precision weight, and the contrasts are moderated by empirical Bayes. quality_weights=True also estimates a weight for every sample from how closely it follows the model (Precision weights). The noisy sample gets a weight well below the others, so it counts less instead of being removed.

DifferentialExpressionResult (limma_voom): 1 group, 1 fitted; 2 contrasts: drug_a_vs_control, drug_b_vs_control
  bulk: 12 samples, 9593 features
  control_1   1.12  ######################
  control_2   1.13  #######################
  control_3   1.10  ######################
  control_4   1.11  ######################
  drug_a_1    1.14  #######################
  drug_a_2    1.12  ######################
  drug_a_3    1.20  ########################
  drug_a_4    1.08  ######################
  drug_b_1    1.05  #####################
  drug_b_2    1.06  #####################
  drug_b_3    0.30  ######
  drug_b_4    1.18  ########################

edgeR’s quasi-likelihood F-tests and DESeq2

fit_quasi_likelihood and quasi_likelihood_f_test take the counts of the genes that pass edgeR’s filter (filter_by_expression) with TMM factors (tmm_factors). fit_deseq2 takes every gene and filters on the mean count when it adjusts the p-values; deseq2_wald_test tests one contrast, and deseq2_likelihood_ratio_test tests both drugs at once against the design with the batch only.

9,593 of 10,000 genes pass the expression filter
quasi-likelihood: common dispersion 0.072, prior df 9.9
DESeq2 likelihood ratio test of both drugs: 563 genes at FDR 5%

Calls compared with the truth

At a false discovery rate of 5%, each method’s calls are compared with the genes the simulation changed: how many it called, how many of those truly changed, and the share of its calls that are false.

contrast           method             called   true  false share
drug_a_vs_control  limma-voom            355    337         5.1%
drug_a_vs_control  edgeR QL              320    312         2.5%
drug_a_vs_control  DESeq2 Wald           360    338         6.1%
                   called by all         320
drug_b_vs_control  limma-voom            204    192         5.9%
drug_b_vs_control  edgeR QL              193    184         4.7%
drug_b_vs_control  DESeq2 Wald           238    203        14.7%
                   called by all         188

The three methods find mostly the same genes: 320 genes are called by all three for drug A. They miss some of the changed genes, as the smallest simulated changes, a fold change of about 1.4, are hard to detect with four replicates. In this simulation DESeq2’s Wald test made more false calls for drug B than the 5% that the adjustment allows on average, also when the noisy sample is left out; one simulated data set is no benchmark, and the Literature lists comparisons on many. Which method to prefer depends mostly on the design and on what comes next: limma’s tables carry moderated standard errors that a meta-analysis combines across studies, and DESeq2’s likelihood ratio test asks about several coefficients at once.