
做转录组分析这几年我身边几乎每个做过RNA-seq的人都被同一个问题卡过拿到表达矩阵之后到底该用DESeq2、edgeR还是limma这个问题如果只看教程每个工具都有一堆拥护者每个都有看起来很厉害的论文背书但真正落到自己的数据上选错工具或者用错参数结果可能就是差异基因列表差出一大截。这篇就专门聊清楚这三条主流差异表达分析路线的底层逻辑、实操流程和选型标准。这篇内容适合谁刚跑通比对和定量、准备做差异分析的入门者换工具时被各种版本差异搞晕的老手以及想把结果汇报写得更有底气的人。我会把三个工具的共同基础、关键差异、具体跑法、常见坑一次讲透尽量让看的人少走弯路。1. 三条路线背后的统计设计逻辑很多人一开始纠结DESeq2、edgeR、limma哪个“最好”这其实是个伪命题。它们本质上是两套完全不同的统计思路只是碰巧都能回答“哪些基因在组间显著差异表达”这个问题。理解了它们在数学上怎么处理计数数据才能明白为什么有些时候它们结果一致有些时候却南辕北辙。1.1 为什么不能用线性模型直接套原始计数RNA-seq的原始数据是read count本质是离散计数。它跟芯片时代的连续荧光信号不一样计数数据有个天然属性均值越小波动越大。一个基因平均表达量是5的时候随机抽样波动可能让某个样本跑到10但平均表达量是5000时波动比例就小得多。这正是泊松分布或负二项分布描述的现象——方差和均值之间存在依赖关系。如果直接把log2(count1)扔进普通线性模型等于假设所有基因的方差是常数。这个假设在RNA-seq数据上不成立会导致低表达基因的假阳性飙升。DESeq2和edgeR的聪明之处在于它们承认方差-均值关系存在并且用经验贝叶斯方法去稳健地估计每个基因的离散度。而limma原本为芯片设计后来加了voom这个转换步骤目的也是把计数数据变成方差稳定的加权线性模型。1.2 三种工具的核心思想速览DESeq2是DESeq的升级版核心是用负二项分布拟合计数再用收缩估计shrinkage把低表达基因、高离散度基因的方差估计往全局趋势上拉。这种收缩让筛选出来的差异基因更保守尤其是小样本量时表现稳。默认输出log2FoldChange是用MAP最大后验做的收缩版本这个细节很多人会忽略。每个基因的离散度先用条件最大似然估计出来再通过拟合一条“离散度-均值”曲线把每个基因的离散度收缩到曲线上。收缩力度取决于该基因的样本量和自身方差稳定性。也就是说本身数据非常稳定的基因收缩很少而数据波动大、样本量小的基因会被大幅度拉向整体趋势。这就是DESeq2在小样本尤其是3个重复下仍然靠谱的核心原因。edgeR的思路类似同样基于负二项模型但离散度估计采用加权条件最大似然Cox-Reid并支持经验贝叶斯稳健离散度robust dispersion。老版edgeR使用精确检验exactTest新版也支持广义线性模型glmQLFTest。如果你的实验设计涉及多因素或多分组用glmQLFTest更合适。limma是从芯片时代流传下来的老牌工具核心是线性模型加经验贝叶斯方差修正。它本身不处理计数分布但voom这个配套算法把log-CPMcounts per million的均值-方差关系建模后生成每个观测的权重再喂给limma的线性模型框架于是就能处理计数数据了。voom-Limma的优势在于计算快、能处理非常复杂的实验设计包括样本间相关性而且用limma自带的一系列稳健方法如robustTRUE对离群样本有额外防御。三个工具使用的标准化策略也不一样。DESeq2用自身中位数比值median of ratios估算size factoredgeR用TMMtrimmed mean of M-valueslimma的voom用CPM加TMM归一化。看到这里你应该明白了它们不是在同一个数据上做不同检验而是每一步归一化、方差估计、检验统计量都有细微差异。这些差异累积起来就是最终结果差异的来源。2. 跑分析前必须搞定的数据准备三个工具都要求输入一个整数计数矩阵通常是featureCounts或STAR/RSEM输出的gene-level count。这里的一个常见误区是直接用FPKM或TPM做差异分析。FPKM/TPM虽然方便样品间比较但它们的计算过程已经做了标准化破坏了计数矩阵的离散分布特性负二项模型无法正确使用。所以无论哪个工具官方文档都建议输入原始整数count。矩阵的行是基因列是样本。行名用Ensembl ID、Entrez ID或Symbol都行但必须唯一。列名要和后续的分组信息严格一致。我习惯在构建矩阵时就把样本名按“组别_编号”命名比如WT_1、WT_2、KO_1、KO_2虽然R脚本里最终会用一个独立的因子向量指定分组但列名可读性强后面排查问题会省很多精力。低表达基因过滤是另一个必须做的步骤。每个工具内部都会对极低表达的基因做一些处理但我强烈建议先自己过滤一轮。edgeR官方推荐用filterByExpr这个方法会根据样本量、测序深度自动计算CPM阈值保证保留的基因至少有足够read支持其基于负二项分布的检验。DESeq2没有专门的过滤函数需要手动用rowSums(counts 10) 最小样本数这类规则比如至少在两个样本里count能到10左右。分组因子要设置好reference水平。在DESeq2和edgeR里默认的因子比较跟因子水平的顺序有关。如果因子levels设成c(“KO”, “WT”)默认比较就是以KO为对照组输出结果里log2FoldChange的正负就反过来了。我见过不少人在这一步栽跟头跑出来的差异基因看着很正常仔细一看方向全部反了原因就是参考组没锁死。保险的做法是显式用relevel或factor(..., levelsc(WT,KO))让WT作为对照组KO作为处理组保证log2FoldChange是“处理组相对对照组的倍数变化”。设计公式也要提前想清楚。最简单的两分组比较写成~group就行。但如果实验包含批次、性别、年龄等信息应当写成~batch group。这个公式对三个工具通用。设计公式的意思是“表达量的变异可以被哪些因素解释”把已知的混杂因素放进模型相当于在数学上扣除批次效应这样主效应估计更干净。不过要注意放进设计矩阵的因素必须是真实的实验协变量不能把样本名或连续编号放进去否则会吃掉主效应的自由度。3. 三大工具实操流程从count矩阵到差异基因列表这一部分直接给可复现的流程。我以一个人为构造的示例数据来走通全流程假设有6个样本三例对照(Ctrl)三例处理(Treat)。你只需把表达矩阵替换成自己的就行。3.1 准备输入文件假设你手上已经有一个名为count_matrix.txt的制表符分隔文件行是基因列是样本。读进来之后先检查维度、缺失值和重复行名。counts - read.table(count_matrix.txt, headerTRUE, row.names1, check.namesFALSE) # 重复行名处理 counts - counts[!duplicated(rownames(counts)), ] # 过滤低表达至少在3个样本中count 10 keep - rowSums(counts 10) 3 counts - counts[keep, ]我滤完低表达基因后一般会瞄一眼过滤前后的基因数量。如果过滤掉太多比如60%以上说明测序深度或比对效率可能有问题得回头看一下bam文件的比对率和featureCounts的assigned比例。低于50%的assigned比例通常意味着注释文件跟参考基因组版本不匹配先解决这个问题再做下游。样本信息表sample_info.txt只要三列SampleName、Group、Batch如果有。Group列写明每个样本属于哪个条件。下面三步走之后三个工具的数据结构就齐了。sample_info - read.table(sample_info.txt, headerTRUE, check.namesFALSE) sample_info$Group - factor(sample_info$Group, levels c(Ctrl, Treat)) # 顺序对齐列名顺序和sample_info的行顺序一致 sample_info - sample_info[match(colnames(counts), sample_info$SampleName), ]3.2 DESeq2标准流程library(DESeq2) dds - DESeqDataSetFromMatrix( countData counts, colData sample_info, design ~ Group ) dds - DESeq(dds) res - results(dds, contrast c(Group, Treat, Ctrl)) res_shrink - lfcShrink(dds, contrast c(Group, Treat, Ctrl), type apeglm)DESeq()这一步默认会执行归一化、离散度估计、拟合GLM和Wald检验。如果你是按照前面步骤准备的count矩阵这里通常不会报错。如果提示“the design formula contains variables with continuous values”检查一下colData里的列是否不小心被读成了数值型。想保留生物学含义上的log2FC用lfcShrink加上apeglm是当前推荐做法。它生成的log2FoldChange不是原始MLE估值而是收缩后的效应值目的是过滤掉低表达基因带来的不稳定的巨大fold change。当然如果你更看重跟别人已有结果的直接对比用不收缩的results()也行但筛选阈值建议同时看log2FC和padj。DESeq2跑完后我用summary(res_shrink)看大概的差异基因数量。一般padj 0.05且|log2FoldChange| 1作为筛选线。这里需要注意|log2FC|阈值完全取决于你的生物学问题有的实验处理效应强1.5或2才合适有的药物处理是弱效应0.5也能反映真实机制。不要盲目套用1。3.3 edgeR标准流程library(edgeR) y - DGEList(counts counts, group sample_info$Group) y - calcNormFactors(y, method TMM) design - model.matrix(~ Group, data sample_info) y - estimateDisp(y, design) fit - glmQLFit(y, design) qlf - glmQLFTest(fit, contrast c(0, 1)) topTags(qlf, n Inf)edgeR的两个检验函数值得说清楚exactTest()适用于简单的两组比较glmQLFTest()适用于多组和复杂设计。我推荐默认用广义线性模型因为即使你的实验只有两组GLM的处理方式在未来扩展对比比如处理组跟对照组外的第三个组比较时不用改代码架构。glmQLFit用的是QL F-test它在基因水平的离散度上又加了一层稳健估计对付离群值多的基因特别有效。topTags输出包含logFC、logCPM、F和FDR。edgeR的FDR是BH校正后的q值对应DESeq2里的padj。它不会自动收缩logFC因此你可能会看到一些padj挺小但logFC巨大的基因——这些往往是低表达基因读长本来就不稳定。如果要更严格的筛选用treat()函数代替glmQLFTest可以设定一个log2FC的置信阈值比如treat(qlf, lfc1)这样检验的是“差异是否大于1倍”而不是“是否不等于0”边缘效应会被过滤。treat配合FDR筛选在生物学重复质量一般的数据上会稳很多。3.4 limma-voom标准流程library(limma) library(edgeR) dge - DGEList(counts counts, group sample_info$Group) dge - calcNormFactors(dge, method TMM) design - model.matrix(~ Group, data sample_info) v - voom(dge, design, plot TRUE) fit - lmFit(v, design) fit - eBayes(fit) topTable(fit, coef 2, number Inf, sort.by P)voom这一步会自动根据均值-方差关系给每个观测算权重。plotTRUE可以让你看到那条平滑曲线拟合得怎么样如果曲线大体走低表示数据是典型的计数型方差模式如果曲线抖动得很厉害或者有个别样本的权重异常说明数据里有离群样本需要警惕。拟合后eBayes()负责做经验贝叶斯方差调制这是limma能“借用全体基因信息”修正单个基因方差的关键步骤。limma的另一个优势是处理paired design配对样本很方便只需把设计矩阵改成包含配对因子。比如~ patient group。这在临床样本的before-after设计中特别常见。类似的协变量在DESeq2和edgeR的GLM里也能放但limma在矩阵层面的处理相对直观一些。3.5 三个工具结果对比该信谁跑完三个工具之后你会发现大部分核心差异基因是重叠的但边缘基因会有差异。这不是“哪个工具错了”而是各自对离散度和显著性的加工方式不同。我自己处理真实数据时常规思路是主结果用DESeq2或edgeR之一limma作为验证。验证的方式不是看差异基因数量谁多而是看共同基因的比例以及log2FC方向是否一致。这里可以补充一个实操技巧把不同工具生成的差异基因列表取交集后再做下游富集分析能有效减少单一统计模型带来的假阳性代价是会漏掉一些边缘效应。而如果实验是筛药或做初步探索我更倾向用相对激进的阈值比如padj 0.05、无log2FC硬性要求先扩大候选范围再用qPCR或Western去验证。统计工具只是帮你排序的助手不是真理裁决机。4. 差异表达结果的核心呈现与解读拿到results对象之后怎么读、怎么展示、怎么筛选这里面的细节直接影响结论可信度。4.1 核心列与解读顺序DESeq2输出列包括baseMean、log2FoldChange、lfcSE、stat、pvalue、padj。看一张结果表首先扫baseMean它代表该基因在所有样本中的平均归一化计数暗示表达丰度。低baseMean的基因即使log2FC很吓人也别太兴奋多半是噪音主导。然后是padj这个列已经做过BH多重检验校正是筛选的主要依据。最后看log2FoldChange注意单位是log2所以1代表2倍2代表4倍。edgeR的topTags输出类似但logCPM代替了baseMeanF和FDR代替了pvalue和padj。limma的topTable则包含AveExpr、t、B、P.Value、adj.P.Val。B值是log-odds表示该基因真实差异的概率正值越大越可靠但实际应用中不如adj.P.Val直观。我建议把三个工具的结果都导出成CSV然后用基因名merge起来做一个“联合评分”列比如计算每个基因在两种工具下都显著的次数。这种多工具投票策略在探索性分析中效果不错也方便在论文methods部分交代清楚所有结果均至少在两种独立差异表达工具中得到验证。4.2 火山图和MA图怎么画一个基础但很受审稿人喜欢的图是火山图横轴log2FoldChange纵轴-log10(padj)。关键信息全在图上右上角是高表达且显著上调的基因左上角是高表达且显著下调的基因中间地带是统计不显著的基因。画图时用ggplot2很容易但要注意给点着色时先设定好阈值逻辑。library(ggplot2) res_df - as.data.frame(res_shrink) res_df$gene - rownames(res_df) res_df$sig - ifelse(res_df$padj 0.05 abs(res_df$log2FoldChange) 1, ifelse(res_df$log2FoldChange 0, Up, Down), NS) ggplot(res_df, aes(x log2FoldChange, y -log10(padj), color sig)) geom_point(size 0.8, alpha 0.6) scale_color_manual(values c(Up #C0392B, Down #2E86C1, NS #BBBBBB)) theme_minimal()MA图则能更好地展示低表达区的高波动问题。横轴是log2(baseMean)纵轴是log2FoldChange。这张图能直观反映收缩效应DESeq2的apeglm收缩结果低表达区的fold-change会明显向0靠拢而edgeR或limma不收缩的结果低表达区会有很多logFC特别大的散点。如果你发现MA图上低表达区一片红显著点集中在左侧大概率就是你用了没有收缩效应结果的默认筛选需要重新考虑阈值或收缩。4.3 差异基因列表导出最后把选定的显著基因配好注释信息导出。我通常用clusterProfiler里的bitr()函数做ID转换把Ensembl ID转成Symbol和Entrez ID然后保存成Excel或者CSV。如果后续要做GSEA还需要一个包含所有基因的排序列表排序指标一般用log2FC与pvalue的组合常用的是-sgn(log2FC) * log10(pvalue)这样可以让上下调基因都参与排序。这个排序文件本身也是结果的体现保留好。5. 实战中躲不开的典型问题与排查方案下面这些坑基本我跑过的每一批数据都碰到过至少一个。整理出来供直接查阅。5.1 分组方向反了如前所述因子水平顺序决定比较方向。很多人看到差异基因数量合理就直接往下做直到做热图时发现对照和处理组表达模式与预期完全相反才发现问题。避免方式在跑完第一步之后先手动打印一下结果里某个已知标志基因的表达方向判定正误再进入批量下游。这比事后返工快得多。5.2 零值过多导致pvalue全是1如果你的数据来自某种低丰度转录本或者在过滤步骤用了过于严格的标准会出现结果里pvalue普遍接近1的奇怪情况。这通常是因为数据结构已经几乎不存在差异检出力。处理手段是把过滤阈值放宽比如rowSums(counts 1) n/2让更多低表达基因进入检验或者检查是不是有样本的测序深度特别低拉低了所有基因的检验效率。低深度样本如果确实无法挽回可以考虑从分析中剔除别硬留着拖后腿。5.3 批次效应明显如果PCA图显示样本不是按分组聚在一起而是明显按批次聚在一起说明批次效应需要处理。最正规的做法是把批次放进design里比如~ batch group然后差异检验仍然看group的系数。对DESeq2来说batch因素的离散度估计和主效应不冲突对limma来说design矩阵自动处理。注意放进去的batch必须是离散变量如果批次是连续数值比如实验日期要转成因子再放否则模型会当成连续协变量解释方式完全不一样。5.4 离群样本在voom的plot或PCA图中如果有个别样本游离在外建议先检查QC指标比对率、基因检出数、rRNA比例。不要急着删样本先把去除与否的结果对比一下。如果删除后差异基因数量变化不大保留样本更能代表实验真实情况如果删除后差异列表显著变化说明这个样本很可能就是噪声源删除更合理。这一决策要在论文里写明。5.5 安装包或版本不一致DESeq2、edgeR、limma的维护频率不一样版本更新有时会改变默认参数比如DESeq2在某个版本后默认的betaPrior参数修改过。好在它们都依赖BiocManager管理复现旧版本环境时可以直接用BiocManager::install(version)指定。实在不行用conda建一个小环境跑R 4.x配上对应版本能把兼容性的头痛降到最低。5.6 无重复样本怎么处理这是新手最容易踩的坑只有两个样本一个对照一个处理照样跑DESeq2。结果出来了但你没有任何办法评估生物学方差离散度只能靠先验估计。RNA-seq差异表达分析对统计功效的客观需求决定了至少3个生物学重复是正常门槛2个重复极端情况下能跑但结果可信度很低。如果实验设计确实受限建议至少设置技术重复并明确告知读者结论是探索性的。实操心得与坑位总结我个人的习惯是任何一批数据先用DESeq2跑一遍主流程同时用limma-voom交叉验证一遍。这个过程多花的计算时间可以忽略不计但能在方法学层面给结论上双保险。真到了多组多因素的复杂设计比如时间序列或含协变量的转录组limma反而会成为首选因为其成熟度最高文档和社区资源也最丰富。还有一点值得反复强调论文汇报差异基因数量时务必写清楚工具版本、参考基因组版本、注释版本、过滤阈值、多重检验校正方法。这些不是可有可无的细节而是决定结果能否被复现的关键信息。我在审稿时见过太多因为一句话“using DESeq2”导致的无法复现最后只能写邮件问作者要参数细节的状况。最后再分享一个小技巧建一个项目专用的R脚本模板把数据读取、过滤、三工具流程、图输出、表导出全部封装好之后任何一批新数据丢进去改一下sample_info就能跑。模板的版本和依赖包固定结果稳定省下的时间足够你再认认真真读几篇文献。转录组分析永远是生物学问题的起点而非终点工具只是帮你把候选基因从两万个缩到几十个真正的重头戏还是后面的功能验证和机制探索。