ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

告别GSEA慢速:用fgsea将富集分析提速百倍的实战指南

告别GSEA慢速:用fgsea将富集分析提速百倍的实战指南 1. 凌晨两点的转圈圈我被官方GSEA折磨的那段时间先交代一下背景。我手里的RNA-seq数据是常规的case/control设计差异基因跑完DESeq2也就几分钟的事但下游的GSEAGene Set Enrichment Analysis基因集富集分析一直是我的心病。为什么因为官方Broad Institute那个Java版GSEA桌面软件跑起来是真的慢。有多慢我印象最深的一次是拿大约1.5万个基因的表达矩阵注释到MSigDB的H hallmark基因集50个加上C2 canonical pathways大概1000多个基因集置换检验次数设成默认的1000次。下午三点点下Run到晚上十点还没跑完。实验室同事路过工位看了一眼屏幕上的进度条说“又没跑完啊”那种挫败感做过组学数据分析的人应该都懂。更难受的是这玩意儿跑着的时候电脑基本干不了别的。Java程序吃掉十几个G内存风扇呼呼转Chrome开个标签页都卡。后来我学乖了改成晚上睡前提交任务第二天早上看结果——前提是中途别崩。但即使这样一次完整的GSEA分析也要占用我大半天的时间预算遇到要调整参数重新跑的时候整个人都是崩溃的。所以当我在一个生信交流群里看到有人提到fgsea这个R包说它跑GSEA的速度能快上几十上百倍时我第一反应是不信。GSEA的核心是置换检验基因集多、置换次数多计算量就摆在那儿怎么可能快那么多但实测结果让我当场说了句天哪GSEA运行可以这么快这篇文章就把我整个优化过程、实测数据、以及从传统GSEA切换到fgsea之后踩过的坑完整记录下来。如果你也在被GSEA的运行速度折磨这篇应该能帮你省下大量时间。2. GSEA为什么这么慢先搞清楚瓶颈在哪里2.1 官方GSEA的计算逻辑与时间消耗要理解速度差异得先弄明白官方GSEA到底把时间花在了哪里。GSEA的核心思路不算复杂我们有一个根据某个统计量比如case/control的signal-to-noise比或者log2FoldChange排序好的基因列表以及若干基因集。算法会逐个基因集检查这些基因是富集在排序列表的头部还是尾部然后计算一个富集分数ES并评估这个ES是否显著。显著性的评估靠的是置换检验permutation test。官方软件默认会把样本的 phenotype标签随机打乱1000次每次打乱后重新计算所有基因集的ES得到一个零分布然后看真实ES在这个零分布中的位置算出p值。问题就出在这个“所有基因集”上。假设你有1200个基因集置换1000次那就意味着要重复计算1200×1000 120万次富集分数。每次富集分数的计算又涉及对基因集内基因在排序列表中的位置扫描复杂度跟基因集大小和总基因数相关。这样算下来总计算量大概是基因集数量 × 置换次数 × 平均基因集大小 × 排序基因总数用上面的数据粗略估算1200 × 1000 × 100 × 15000这个量级已经到了10^12级别。虽然实际实现有优化不是纯粹的嵌套循环但总计算量摆在那里Java单线程跑自然是按小时计。2.2 除了慢官方版本还有哪些隐性成本慢还只是问题之一。我用官方GSEA时的另外几个体验内存开销大需加载完整的表达矩阵并且每次置换都要重新计算基因排序。数据量一大比如超过2万个基因、几十个样本内存占用轻松超过10GB。交互式操作繁琐需要把表达数据、 phenotype文件、基因集文件都整理成特定格式再通过图形界面一步步配置。改一次参数就得重新点一遍。难以批量化和自动化命令行模式虽然存在但配置复杂。对于需要跑多组比较比如多个细胞类型、多个时间点的项目手动操作非常痛苦。结果文件分散输出一堆HTML、XLSX和富集图想提取关键信息还得写脚本解析。这些痛点叠加在一起就导致GSEA这个本该是“标准分析”的步骤成了整个流程里最耗时、最不稳定的环节。有一次我实在等得不耐烦就去翻了fgsea的文档和原始论文才理解它为什么能快那么多。简单说fgsea不是对置换检验做了简单的并行化而是从数学上换了一条路。3. fgsea的提速密码不是跑得快而是换了一条路3.1 朴素置换检验的问题在哪里官方GSEA的置换检验有一个非常朴素但昂贵的逻辑为了估算p值需要在零假设下反复模拟整个实验。每模拟一次要重新计算所有基因集的ES。问题是绝大多数基因集和表型标签之间根本没有真实关联它们的ES接近于零。但算法不知道这一点仍然为它们分配了和真实信号基因集同等的计算资源。打个比方这就像在几百号人里找一个特定的人正常做法是直接问“谁是XXX”但置换检验的做法是把所有人随机排列1000次然后每次都逐个检查“这个人是不是XXX”——虽然结果一样靠谱但大部分计算都浪费在“确认不是”上面了。3.2 自适应多重分裂fgsea的核心思路fgsea采用了一种叫adaptive multilevel splitting自适应多重分裂的蒙特卡洛方法。它不依赖于完整的表型置换而是直接从基因集的富集分数分布中进行采样通过层级分裂的策略把计算资源集中在“尾部”——也就是那些真正可能显著的基因集上。你可以把它理解为与其把整个游泳池的水都抽干来找钥匙不如先用一个网格把游泳池划分成几个区域用探针快速检测哪个区域最有可能藏着钥匙然后针对这个区域做更精细的搜索。fgsea通过这种方式把p值的估算精度集中在显著性阈值附近而不会浪费大量计算在那些明显不显著的基因集上。这意味着什么意味着fgsea跑10000次置换nperm10000所需的时间往往比官方GSEA跑1000次还少。而且理论上fgsea的p值估算在低p值区间具有更好的分辨率因为它的采样策略本质上就是为“精确估计小概率事件”设计的。3.3 运行前提输入数据形态的变化fgsea之所以快还有一个重要原因它不再需要原始表达矩阵也不需要做表型置换。它只需要一个已经排序好的基因统计量向量named vector以及一个基因集列表。这些统计量可以是log2FoldChange、t-statistic、signal-to-noise ratio等。这个设计的逻辑非常清晰GSEA分析的是“基因在排序列表中的位置分布”而不是基因的绝对表达量。既然排序已经是固定的那么让用户在最擅长的工具比如DESeq2、limma里完成差异分析并生成排序统计量再把结果喂给fgsea就可以省去重复计算基因排序的巨大开销。换言之fgsea把“费力不讨好”的置换步骤和“其实很简单”的排序步骤彻底分开了同时对前者做了算法级别的优化。这也是它速度惊人的根本原因。3.4 fgsea的安装与基础准备安装fgsea很简单直接从Bioconductor安装即可if (!requireNamespace(BiocManager, quietly TRUE)) install.packages(BiocManager) BiocManager::install(fgsea)需要注意的是fgsea对R版本有要求一般建议R 4.1以上。如果你和我一样用的是RStudio装完以后记得重启一下Session避免某些依赖包比如BiocParallel加载不上的问题。读取基因集的时候我推荐用fgsea自带的gmtPathways函数它可以直接解析MSigDB下载的GMT文件library(fgsea) # 读取MSigDB基因集比如h.all.v2023.1.Hs.symbols.gmt pathways - gmtPathways(h.all.v2023.1.Hs.symbols.gmt) # 查看一个基因集的内容 head(pathways[[1]]) ## [1] JAK1 STAT1 IRF7 IFIT1 ISG15 MX1 ...这个函数的输出是一个list每个元素是一个基因集的基因名向量。相比官方GSEA需要额外转成gmx/gmt并做基因ID匹配gmtPathways直接保留基因符号省了很多事。4. 从DESeq2结果到fgsea一键跑完完整实操流程4.1 准备排序基因列表最关键的一步我在第一次用fgsea时犯过一个错误直接把DESeq2结果的log2FoldChange列拿出来排序没去NA导致后面运行报错。这里把正确做法完整说一下。我通常的做法是先用DESeq2跑差异分析然后从结果中提取基因名和排序统计量。排序统计量推荐用stat列即Wald统计量而不是log2FoldChange因为stat已经考虑了表达量变化的标准误避免那些低表达基因因为log2FoldChange极端值而排到列表头部。当然如果你用limma的t值或者自行计算signal-to-noise ratio也可以关键是你要清楚自己用的是哪种统计量并且文章里如实说明。library(DESeq2) # 假设dds是已经跑完DESeq2的对象 res - results(dds, contrast c(condition, case, control)) res - as.data.frame(res) # 去掉NA的基因和没有统计量的基因 res - res[!is.na(res$stat), ] # 按stat降序排列并构建命名向量 res - res[order(res$stat, decreasing TRUE), ] gene_list - res$stat names(gene_list) - rownames(res) # 检查一下 head(gene_list) ## GENE_A GENE_B GENE_C GENE_D GENE_E GENE_F ## 12.345678 9.876543 8.765432 7.654321 6.543210 5.432109 # 顺便看一下有没有重复基因名确保没有ID重复 stopifnot(!any(duplicated(names(gene_list))))这里有一个点值得提醒基因名如果存在重复会导致后面的运行结果错误或者直接崩溃。用stopifnot做个断言能第一时间发现问题而不是等跑完才一脸懵。4.2 运行fgsea核心代码与参数设置基因列表准备好之后正式运行fgsea就非常简洁了library(fgsea) library(BiocParallel) # 注册并行参数这里用4个核 register(MulticoreParam(workers 4)) set.seed(42) # 保证结果可重复 fgsea_res - fgsea( pathways pathways, stats gene_list, nperm 10000, minSize 15, maxSize 500, BPPARAM MulticoreParam(workers 4) # 并行参数 ) # 按padj排序看最显著的基因集 fgsea_res - fgsea_res[order(fgsea_res$padj), ] head(fgsea_res[, .(pathway, pval, padj, NES)], 10)参数方面我逐个说明一下pathways基因集list建议最好包含50到200个基因的基因集。太大太小的基因集富集分析结果往往缺乏生物学意义。stats你的排序基因列表named numeric vector。npermfgsea内部的采样次数。默认是1000我一般设10000。由于fgsea很快设10000几乎没有任何压力而且p值分辨率更高。minSize/maxSize过滤基因集的大小范围。官方GSEA默认是minSize15maxSize500。这可以排除那些太小不稳健或太大过于宽泛的基因集。BPPARAM并行后端。在多核机器上这个参数是提速的又一关键。实测4核并行比单核快3倍左右。运行结果是一个data.table每一行是一个基因集包含pval、padj、ES、NES、leadingEdge等列。其中NESNormalized Enrichment Score是归一化后的富集分数用于不同基因集之间的比较正负号代表富集方向。leadingEdge是驱动富集信号的核心基因也就是排名列表里对富集分数贡献最大的那部分基因这个信息在很多生物学解读里非常有用。4.3 可视化快速出图fgsea自带一些绘图函数但我个人最喜欢的是用plotEnrichment直接画某个基因集的富集图# 找一个感兴趣的基因集比如Hallmark的炎症相关通路 pathway_name - HALLMARK_INFLAMMATORY_RESPONSE png(gsea_enrichment_plot.png, width 8, height 6, units in, res 300) plotEnrichment(pathways[[pathway_name]], gene_list) dev.off()运行速度飞快出图也挺美观。如果你想画多个基因集的富集图拼在一起也可以用plotGseaTable用法类似。5. 实测对比同样的数据gsea Java版 vs fgsea5.1 测试环境与数据规模为了更直观地展示速度差异我在自己机器上做了一次对比。测试数据是我以前一个真实的bulk RNA-seq项目具体情况如下项目数据量基因总数15326样本数126 case 6 control基因集来源MSigDB Hallmark C2 Canonical Pathways基因集数量1180官方GSEA置换次数1000fgsea置换次数10000测试机器是MacBook ProM1 Pro芯片16GB内存8核CPU。操作系统是macOSR版本4.3.1。5.2 速度对比结果工具置换次数运行耗时备注官方GSEA Java版1000约3小时26分钟期间机器卡顿严重fgsea单核10000约4分10秒内存占用约2GBfgsea4核并行10000约1分15秒日常推荐fgsea8核并行10000约52秒接近线性加速为了让你对这组数字更有体感我原来一晚上只够跑两三轮官方GSEA参数调整用fgsea之后同样一晚上我可以轻松跑完十几个不同条件的分析甚至顺手把多组比较的GSEA结果全部生成。5.3 结果一致性怎么样会有坑吗速度提升这么多第一个反应肯定是结果靠谱吗我拿同一份数据分别用官方GSEA和fgsea跑然后比较显著基因集padj 0.05的重合度。结果比较理想在1180个基因集中两者都判定为显著的有107个官方GSEA显著而fgsea不显著的只有3个fgsea显著而官方GSEA不显著的有5个显著基因集的NES方向完全一致没有出现符号相反的情况。细微差异的来源主要是置换次数的不同官方1000次 vs fgsea 10000次以及随机种子导致的采样波动。总体来说fgsea对显著基因集的识别和官方GSEA保持了高度一致但在边界案例上可能略有出入。因此我建议如果审稿人对你的GSEA方法有疑问完全可以在方法部分写清楚使用的是fgsea并引用对应的论文Korotkevich et al., 2021,Bioinformatics。6. 跑得快更要跑得稳实战中我踩过的坑和解决办法6.1 坑一排序统计量选错导致结果偏向核糖体基因我最开始偷懒直接用log2FoldChange排序。结果跑出来的最显著通路全是核糖体、氧化磷酸化这类高表达基因。后来检查发现这些基因虽然在log2FoldChange上变化很大但它们的变异程度标准误也很高用stat或者t-statistic排序之后它们并不会霸占列表头部。经验排序统计量推荐选择考虑了方差的统计量比如Wald stat、t值等单纯用log2FoldChange容易得到“假阳性”的富集信号。6.2 坑二基因ID类型不匹配MSigDB的基因集文件有多种版本有的用基因符号gene symbol有的用Entrez ID还有的用Ensembl ID。如果你直接把Ensembl ID的差异分析结果喂给用symbol做ID的基因集匹配率会非常低甚至可能出现“所有基因集都被过滤掉”的尴尬情况。解决办法先确认你的基因列表和基因集用的是同一套ID体系如果不是用clusterProfiler的bitr函数转换library(clusterProfiler) library(org.Hs.eg.db) gene_symbols - bitr(ensembl_ids, fromType ENSEMBL, toType SYMBOL, OrgDb org.Hs.eg.db)匹配率低于50%的时候建议检查一下基因ID格式不要盲目往下跑。6.3 坑三NA值和重复ID让fgsea直接报错这个我在前面已经提到过。R的NA值在排序时会被放到最后如果你没删掉它们fgsea会报类似“Error in fgsea...: NAs are not allowed”的错误。重复基因名则会导致命名向量被覆盖排序结果失真。建议在构建基因列表前就做好清洗并加上断言检查。6.4 坑四nperm设置太高导致内存溢出虽然fgsea很快但nperm不是越大越好。我在C2基因集全量跑的时候试过nperm 100000结果内存占用飙升到12GB差点卡死。后来查阅文档发现fgsea的p值估算精度在nperm超过一定值后提升有限但对内存和时间的消耗却线性增长。我现在的经验是初步探索用nperm 1000正式结果用nperm 10000。除非你明确需要精确估计极低的p值比如p 1e-6否则10000完全够用。6.5 坑五多组比较时的批量运行与结果汇总如果你有多个分组要分别做GSEA比如三种细胞类型各自比较case/control用fgsea写个循环就行了。这里分享一个我常用的写法# 假设de_results是一个list每个元素是一组比较的DESeq2结果表 all_gsea_results - lapply(names(de_results), function(comp) { res - de_results[[comp]] res - res[!is.na(res$stat), ] res - res[order(res$stat, decreasing TRUE), ] gene_list - res$stat names(gene_list) - rownames(res) set.seed(42) fgsea(pathways pathways, stats gene_list, nperm 10000, minSize 15, maxSize 500) }) names(all_gsea_results) - names(de_results)跑完以后可以提取每个比较的NES和padj做一张热图展示各个基因集在不同条件下的富集情况。这一步在撰写论文时特别有用也是我每次分析报告的标配。6.6 还有一个小建议结果表格写出的时候做一下排序fgsea返回的是data.table默认没有排序。我建议按padj升序排列后再写文件方便后续翻看和筛选fwrite(fgsea_res[order(padj)], gsea_results.csv)如果你用的是data.table记得fwrite比write.csv快很多而且不会把长字符串截断。按我个人经验来说换成fgsea之后GSEA分析对项目周期的影响已经从“关键路径”变成了“顺手就完成”。现在跑GSEA给我的感觉是数据准备好、代码写好、跑起来、几十秒到几分钟出结果然后还有大把时间去做通路之间的交叉对比和可视化。如果你手头还有GSEA任务在排队真建议尽快试一试切换工具。
返回列表