资讯动态

limma多组差异分析全流程:线性模型、对比矩阵与经验贝叶斯

发布时间:2026/9/15 16:18:30 来源:尧图企业网站定制
做多组转录组差异分析时我最常被问到的一个问题是“limma是不是只能用在两组比较多组是不是要拆开跑两两t检验”早几年我自己也是这么干的三组样本就两两比三次然后手动合并结果。后来有一次投稿审稿人问了一句“你用的是多组设计为什么每个基因没有整体检验你这样做如何控制假阳性”我才真正把limma的线性模型框架吃透。limma这套工具从设计之初就是为多组比较、多因素实验服务的它的核心不是t检验而是用设计矩阵加对比矩阵做统一建模再靠经验贝叶斯方法稳定大幅值方差。这篇文章我会完整过一遍用limma做多组差异表达分析的流程包括为什么不要拆开跑两两比较、设计矩阵怎么理解、contrast怎么写、F检验和后续两两比较怎么配合以及我实际跑数据时踩过的坑和规避方法。适合刚拿到counts矩阵、组数在3组以上、正在纠结“多组到底怎么比最合理”的人参考。1. 多组比较为什么推荐线性模型整体检验1.1 拆成两两t检验的问题出在哪假设你有三组样本对照组、处理A组、处理B组每组4个生物学重复。最直觉的做法是对照组和处理A组跑一次差异分析对照组和处理B组再跑一次差异分析甚至有人会把处理A和处理B也跑一遍。相当于做了3次独立的两两比较。可是这么做有三个问题。第一个问题是假阳性膨胀。每次比较在0.05的显著性水平下出现一次假阳性结果的概率是5%。做3次独立比较至少出现一次假阳性的概率就是1 - (1 - 0.05)^3 ≈ 14.3%。组数越多这个值涨得越快。5组时理论值就接近40%。很多人在多组实验中发现“差异基因数量多得离谱”往往不是生物学真相而是重复检验累积出来的统计假象。第二个问题是每个比较单独估算方差数据利用不充分。在t检验里某组基因的组内方差只用本组的重复样本估算。4个重复意味着方差估计只有3个自由度非常不稳定。但其他组的样本其实也能反映同一基因的表达波动水平仅因“不属于这次比较”就被丢弃了很浪费。组学数据又是典型的小样本高维数据浪费信息等于放大噪声。第三个问题是逻辑上不自洽。多组实验本身是一个整体设计实验要回答的问题是“这些处理对基因表达产生了怎样的影响”。拆成多次两两比较等于把这个整体问题切碎成若干独立问题组间差异的结构信息比如组间方差大小的比较、多组共同的分组结构全都被丢掉了。1.2 limma为什么更适合多组场景limma的做法和拆开跑完全不一样。它对每个基因拟合一个线性模型模型里同时放进所有组别然后一次性估计系数矩阵、残差方差再用经验贝叶斯方法对方差进行整体压缩。这里的关键是经验贝叶斯。limma假设成千上万个基因的方差并不是孤立的而是服从一个共同的先验分布。每个基因的方差估计会被拉向全体基因方差的平均水平也就是所谓的“方差收缩”。基因自身的方差估计仍然保留权重但不会再像单纯的t检验那样因为某组重复数的方差碰巧很小而给出一个虚高的t统计量。这个特性在重复数少的时候尤其重要相当于用全基因组信息帮着稳住单个基因的统计推断。多组场景下limma还会为每个基因计算一个类似ANOVA的F统计量对应一个整体检验先回答“这个基因在任意组之间的均值有没有显著差别”。这等于先做一次全局初筛如果F检验都不显著后续的两两比较通常也不会得到稳定可信的结论。只有先锁定那些确实存在组间表达变化的基因再去细看具体是哪两个组有差异逻辑上才立得住。注意这里说的F检验不是让你做完F检验后再挑变化最大的基因做两组t检验。正确的顺序是在同一个线性模型框架里F检验和contrast对比检验共享同一套方差估计先看整体显著性再由contrast定位差异来源。两部分是一体的不是两套独立分析。2. 准备工作表达矩阵、分组因子与RNA-seq数据预处理2.1 先把三样东西准备好多组差异分析开始之前有三样东西必须确认清楚表达矩阵、样本信息表、分组因子的水平顺序。这三样看似基础却是后面设计矩阵和contrast能正确运行的前提。表达矩阵是基因乘以样本的数值矩阵行名是基因ID列名是样本名里面的数值代表基因表达定量结果。如果你做的是芯片数据通常是归一化后的信号强度如果做的是RNA-seq而是基于counts转化出来的logCPM、TPM或者经过voom转换后的表达值这里需要特别留意后面会细说。矩阵里不能有缺失值如果有必须提前决定是填补还是剔除。样本信息表包含至少两列一列是样本名另一列是分组信息。关键点在于分组列应当显式设置为因子类型并且要手动指定水平的顺序。group - factor(group, levels c(Ctl, Mut, Treat))这样写limma才知道“对照组”是哪一个后续输出结果里的列名、排序、设计矩阵的列顺序都会被这个因子水平顺序影响。最怕的是直接用read.table读进来R自动按字母顺序排水平导致“Mut”排到了“Ctl”前面后续写contrast时非常容易出错。2.2 counts数据不要直接丢进lmFit如果是RNA-seq数据原始counts矩阵不能直接拿去跑lmFit。counts数据服从近似负二项分布均值越大方差越大直接用线性模型默认的等方差假设分析相当于让高表达基因拥有过大的权重结果会产生系统性偏差。正确的做法是用limma包里的voom函数切换。voom的本质是先把counts转成logCPM再根据均值方差关系给每个观测值估计一个精度权重之后limma的线性拟合才能正确处理RNA-seq数据。我自己的习惯是先用edgeR或limma自带的cpm函数做一步低表达基因过滤。一个比较常用的标准是要求某个基因至少在最小样本组的一半样本中CPM大于1。操作上可以先算好每个组样本数的最小值然后用rowSums(cpm(expr) 1) minGroupSize去过滤。这一步不是为了追求统计上的精细而是为了减少下游多重比较校正的负担同时去掉那些本身表达量极低、生物学上很难稳定检测的基因。过滤之后的矩阵再进入voom流程。关于数据标准化还有一个小点如果你用的是TPM或者FPKM就不需要走edgeR的cpm过滤逻辑了因为问题已经不在“测序深度”层面。但直接用TPM矩阵跑limma时最好还是确认一下表达值范围如果数值跨度很大先做log2转化再建模会更稳。3. 设计矩阵与contrast矩阵多组差异分析的核心参数3.1 两种设计矩阵写法选哪种心里要有数设计矩阵是整个线性模型的地基。limma中对多组设计最常用的是这个写法design - model.matrix(~0 group)这个写法会生成一个n行k列的矩阵k是组数每一列对应一组的均值参数列名会被自动设置为组名。它的好处是直观设计矩阵里的每一列就是“这个样本是否属于这一组”的指示变量。做contrast的时候加减关系一目了然比如Mut - Ctl就是“突变组均值减对照组均值”。另一种写法是带截距的参数化方式design - model.matrix(~group)这种方式会生成一列截距和k-1列差值列。截距对应的是第一个水平的组均值剩下的列表示其他组相对于第一组的差异。如果只有两组这种写法非常方便直接lmFit、eBayes就能得到想要的比较结果。但在多组场景下它会让“哪个组是参考组”这件事变得隐晦而且写contrast时不够直接容易把自己绕晕。所以我的建议很简单凡是组数大于等于3一律用~0group的写法把所有组均值显式列出来然后用contrast矩阵定义比较。这会让后续脚本更可读自己三个月后再看也还知道在比什么。3.2 contrast矩阵怎么构造设计矩阵解决的是“模型由哪些参数组成”的问题contrast矩阵解决的是“你要比较哪些参数”的问题。limba中用makeContrasts函数来构造比较关系con - makeContrasts( Mut_vs_Ctl Mut - Ctl, Treat_vs_Ctl Treat - Ctl, Treat_vs_Mut Treat - Mut, levels colnames(design) )这里面每一行等式定义了一个比较。等式左边的名字是自己起的会出现在后续所有输出结果中等式右边必须由设计矩阵的列名组成不能随意写。levels参数固定填colnames(design)这是确保列名匹配的铁律。多组比较时对比的数量最大可以是组数乘以组数减一的一半比如3组最多3个两两比较4组最多6个。但并不是每个实验都需要穷举所有两两比较。需要看到具体哪几对差异决定了你要定义几个contrast。比如你只关心“处理是否逆转了突变表型”那就只需要定义Mut_vs_Ctl和Treat_vs_Mut两个对比不必把Treat_vs_Ctl也硬塞进去。少一个比较结果就少一次多重检验负担统计上更干净。3.3 多因素实验可以往设计矩阵里追加协变量多组实验经常伴随批次效应或性别、年龄等协变量。如果批次信息是已知的直接加到设计矩阵里就行比如design - model.matrix(~0 group batch)这样模型会把批次效应作为截距之外的附加变量估计掉组间差异的估计就等同于“在批次一致的前提下比较组间差异”。这个思路和多组设计的整体检验是一致的只要把批次变量一并放进设计矩阵limma会自行处理。注意批次变量也要是因子并且批次内部不能和分组完全混淆否则模型会出现不可估计的问题。4. 完整实操一个三组差异表达的limma标准流程4.1 从表达矩阵到差异基因的完整R代码这一节我直接给出一套可复制的完整流程以三组对照组、突变组、药物处理组每组4个生物学重复为例。library(limma) library(edgeR) # 1. 读取数据 expr - read.table(counts_matrix.txt, header TRUE, row.names 1, sep \t) metadata - read.table(sample_info.txt, header TRUE, sep \t) # 2. 分组因子显式指定水平顺序 group - factor(metadata$condition, levels c(Ctl, Mut, Treat)) # 3. 过滤低表达基因 min_group_size - min(table(group)) keep - rowSums(cpm(expr) 1) min_group_size expr - expr[keep, ] cat(保留基因数, nrow(expr), \n) # 4. 使用 voom 转换 counts 数据 design - model.matrix(~0 group) colnames(design) - levels(group) v - voom(expr, design, plot TRUE) # 5. 构建对比矩阵 con - makeContrasts( Mut_vs_Ctl Mut - Ctl, Treat_vs_Ctl Treat - Ctl, Treat_vs_Mut Treat - Mut, levels colnames(design) ) # 6. 线性拟合 对比 经验贝叶斯 fit - lmFit(v, design) fit2 - contrasts.fit(fit, con) fit3 - eBayes(fit2) # 7. 查看整体F检验显著的基因 topTable(fit3, coef 1:3, number 10, sort.by F) # 8. 提取特定比较的结果 res_mut - topTable(fit3, coef Mut_vs_Ctl, number Inf, adjust.method BH) res_treat - topTable(fit3, coef Treat_vs_Ctl, number Inf, adjust.method BH) # 9. 差异基因计数 results - decideTests(fit3, method nestedF, adjust.method BH, p.value 0.05) summary(results)4.2 这段流程里每一个关键步骤的逻辑第3步过滤低表达基因我用的标准是“在至少一个完整最小样本组内CPM大于1”。这里强调最小样本组是为了避免某个处理组重复数特别少时过滤标准过于苛刻。这一步不是必须的但过滤之后voom对均值方差关系的估计会更稳定也能减少后续多重检验的基因总数提高检出效率。第4步voom转换是RNA-seq跑limma的关键跳板。voom做好两件事把counts转成logCPM让表达值的分布更接近正态再根据均值方差趋势为每个基因的每个样本计算权重。这个权重会进入lmFit相当于对高方差的低表达基因做了降权避免它们干扰整体模型。实际使用时建议把plotTRUE打开观察那张均值方差图正常情况下应该是一条随均值先升后降的曲线如果曲线形状扭曲说明过滤可能有问题或数据里有极端样本。第6步三行代码是有顺序逻辑的lmFit先用最小二乘拟合每个基因的线性模型得到系数和残差contrasts.fit再把你定义好的contrast作用到系数上计算每个对比的估计值和标准误eBayes最后做经验贝叶斯方差收缩并以此为基础计算t统计量、F统计量和p值。很多人把eBayes当成一个可有可无的常规动作其实它才是limma区别于普通线性模型的核心环节少了这一步你拿到的只是普通的普通最小二乘结果方差没有被修正小样本下极易出现极端p值。第8步提取结果时我习惯给topTable加一个numberInf而不是默认的10这样会返回所有基因的结果方便后续用p值和logFC自己筛选。同时设置adjust.methodBH用Benjamini-Hochberg方法校正多重比较。output文件里adj.P.Val列就是可以用于筛选的FDR值。这里有个容易被忽略的细节如果第7步用coef1:3topTable返回的表格里会有一列F统计量和对应的p值这个F列是“所有contrast联合是否显著”的整体检验。而第8步单独指定一个coef时topTable返回的则是该对比的logFC、t统计量和p值不再有F列。理解了这一点就理解了多组分析中整体与局部的关系。5. 结果解读F检验、两两比较与差异基因筛选5.1 F检验是多组比较的“总开关”多组设计的limma输出中F检验非常关键。对于一个基因若设计中有k个组那么它对应的所有组均值是否完全相等的检验本质上是一个有k-1个自由度的F检验。limma在计算F统计量时会使用经验贝叶斯收缩后的方差所以它优于直接对counts做ANOVA。实际操作中我会先用topTable(fit3, coef1:3, sort.byF)看一遍基因列表此时的p值对应的原假设是“该基因在所有组中表达均值无差异”。如果F检验都不显著那它基本没有进入下游分析的资格。这个初筛思路就相当于先把几万个基因压到几千个候选基因再让contrast来定位具体是哪个比较造成的差异。但要注意一点F检验显著不代表每一个contrast都显著。它可能是任何一组偏离其他组引起的也可能代表一种组合效应。所以F检验筛出来的基因还要逐一检查各个contrast的logFC和p值才能回答“到底是处理A和对照组有差异还是处理B和处理A也有差异”。5.2 用decideTests做差异基因分类除了用topTable一列列看结果limma还提供了一个分类工具decideTests。decideTests(fit3, methodnestedF)会返回一个矩阵行是基因列是各个contrast数值为1代表在该对比中显著上调-1代表显著下调0代表不显著。nestedF方法的思路很简单一个基因只有先通过全体contrast的F检验相当于整体关才允许比较单个contrast是否显著。这样做等于把整体检验和两两比较合并成了一个决策流程从统计上减少了对组间差异过度解读的风险。这个方法还有一个重要应用后续画维恩图或做热图时可以直接用这个矩阵统计每个对比中差异基因的上下调数量也能快速找出只在某个特定对比中出现的基因。比如三组比较的典型输出可能是“共有347个基因在任意比较中差异其中突变vs对照208个药物处理vs对照192个两者共有137个”。这样的统计结果比单独拼凑三份topTable列表要可信得多。5.3 差异基因筛选阈值怎么定多组差异分析中筛选差异基因没有统一阈值但转录组应用里最常用的是FDR 0.05 且 |log2FC| 1。log2FC等于1相当于表达量变为原来的2倍等于-1相当于表达量变为原来的0.5倍。这个阈值控制的是生物学显著性标准不是唯一标准。如果实验目的是挖出尽可能多的候选基因可以把log2FC降到0.585也就是1.5倍差异如果目的是找核心驱动基因甚至可以要求在多个比较中都达到FDR0.01且|log2FC|2。我的习惯是先看整体F检验筛选出的基因数量再叠加任意一个contrast的显著性和倍数变化阈值。如果整体F显著但所有contrast都不显著这种基因往往代表一种多组间差异的复杂模式不适合用简单的两两逻辑去解读。更稳妥的办法是把这类基因单独挑出来做聚类或通路分析看它们是否富集在某个生物学过程中。这里推荐一个实践技巧把topTable输出按F统计量排序画一张整体F值的火山图横轴是AveExpr纵轴是-F的log10 p值可以非常直观地看出“哪些基因在任何组间都有强变化”。这张图比两两比较的火山图更能体现多组设计的整体视角审稿人也更容易理解。6. 高频问题排查与发表文章的方法描述6.1 我实际踩过的几个坑整理成速查表多组差异分析看起来代码不长真正跑的时候坑都在细节里。我把这几年来最常遇到的问题整理成一张速查表基本都是可以自查的。问题表现常见原因解决办法makeContrasts报错“object not found”等式右侧的组名和设计矩阵列名不一致常见大小写或空格差异先运行colnames(design)核对列名再写contrasttopTable结果里全是NA指定了不存在的coef编号或者contrast矩阵中列名写错用colnames(fit2$contrasts)查看fit2中的对比名eBayes后系数为NA或提示线性依赖设计矩阵的列线性相关常见原因是有样本被重复分组或者协变量与分组完全混淆检查design矩阵用alias(fit)查看哪列线性依赖F列全为空提取结果时只指定了一个coef此时limba不会输出F检验用coef1:ncol(fit2$contrasts)或coef所有对比名的向量差异基因数量异常多没有过滤低表达基因或者直接用原始counts进入lmFit方差被高表达基因主导先过滤再用voom检查均值方差图同一脚本换数据后对比结果错位分组因子的levels顺序没有显式指定R按字母顺序重排了始终用factor(..., levelsc(...))显式指定顺序有一个印象特别深的坑某次用三组数据跑出来突变组vs对照组的差异基因有4000多个明显不合理。后来排查发现我在写design时用的是model.matrix(~group)没有显式控制levelsR默认把字母序最靠前的“Ctl”当成了参考组这本身没问题但我在makeContrasts里又用Mut-Ctl写了对比结果设计矩阵中截距本身代表Ctl均值Mut-Ctl这个等式把“截距差分”减“截距”的隐含关系重复计算了最后结果完全错乱。从那以后我无论几组一律先用model.matrix(~0group)再检查一遍colnames(design)然后才写contrast。这个习惯帮我省掉了大量排查时间。6.2 论文方法部分怎么写才算清晰用limma做完多组差异分析后论文方法部分至少要交代清楚几个信息使用的是limma包的哪个函数、版本号、表达量类型、分组模型、contrast定义、多重校正方法以及筛选阈值。一个相对完整的写法可以这样组织差异表达分析使用R软件中的limma包版本3.5x.x完成。首先使用edgeR中的cpm函数过滤低表达基因保留至少在最小样本组中有超过1 CPM表达的基因随后使用voom函数对counts数据进行均值方差建模并转换线性模型采用~0condition构建设计矩阵并使用makeContrasts定义组间比较经验贝叶斯方法用于方差收缩多重比较校正采用Benjamini-Hochberg方法。差异基因的筛选标准为校正p值小于0.05且|log2FC|大于等于1。这段描述读起来平淡但基本覆盖了复现所需的全部要素。如果需要额外说明批次校正可以在建模部分补一句“将批次作为协变量纳入设计矩阵”。另外一定记得在脚本末尾保存sessionInfo()包括limma和edgeR的版本号审稿人如果要求补充方法细节这份记录会非常有用。6.3 再多说一点多组比较结果的呈现分析做完了结果呈现也有讲究。多组差异分析最常见的呈现方式是三个contrast结果叠在一起的热图或火山图。热图我喜欢按整体F检验排序取FDR最小的前50个基因画图行做标准化列按组别分组这样能直观看到突变组和药物处理组对基因表达的不同影响模式。火山图则适合分别绘制每个contrast但要注意每个图里标注清楚比较的对象。如果想展示多组之间差异基因重叠情况可以直接用limma的vennDiagram(results)。但注意vennDiagram对三组及以上组合时会很拥挤更推荐用UpSet图或集合统计表来展示。我的习惯是给一个三列统计表列出各对比单独差异基因数、两两重叠数和三者共有数信息密度比维恩图高还不会因为图幅问题看不清。关于后续扩展还有一个提示如果组别之间存在自然顺序比如时间点0h、6h、12h、24h那么可以考虑用contrast来构造线性趋势检验比如Linear (Time12h - Time6h) - (Time6h - Time0h)这样的二型对比。但这属于时序分析范畴和普通的无序多组比较思路不完全一样本文不展开。如果你只是处理3到5个无序分组样本上面的流程已经足够应付绝大多数场景。说回整体思路。用limma做多组差异表达分析最核心的一点是别把它当成“两两比较的高配版”。它是先从线性模型整体检验入手再让contrast去回答具体比较问题的完整推断框架。我后来每次拿到多组数据都会先跑一遍F检验生成整体显著基因列表再逐一看两两比较结果。这个习惯让我核实了很多“看起来很显著”的比较其实只是因为之前的模型没有把全局方差压缩好。分析多组数据时整体、分部两步走逻辑清楚结果也才站得住脚。按照这个流程走一遍你大概率能把多组差异分析做得既快又稳。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价