
1. 为什么单细胞代谢分析值得你花时间折腾做单细胞测序数据分析的人迟早会撞上一堵墙聚类分群做完了细胞类型注释也搞定了差异表达基因列表拉出来一看全是熟悉的那些免疫检查点、细胞因子、转录因子。然后呢然后很多人就卡在这里了不知道下一步该往哪个方向挖。我刚开始做单细胞分析那会儿也是这样手里攥着几万细胞的表达矩阵翻来覆去就是看那几个经典marker总觉得浪费了数据。后来接触到代谢层面的分析思路才意识到一个很关键的问题细胞的代谢状态往往比基因表达本身更能反映它的功能倾向。一个T细胞到底是处于激活态、耗竭态还是记忆态光看表面marker有时候真的分不清楚但如果你去看它的代谢通路活跃程度很多模糊的地带就变得清晰了。这就是COMPASS这个工具切入的角度。COMPASS的全称是Cellular Metabolism at Single-cell level via Penalized Regression翻译过来就是“通过惩罚回归在单细胞水平上刻画细胞代谢”。它做的事情说起来不复杂给定一个单细胞的基因表达矩阵结合已知的代谢反应网络比如RECON2这类重构的代谢模型通过一种带惩罚的回归方法推断出每个细胞在各个代谢反应上的“活跃概率”。注意它给出的不是简单的通路富集分数而是一个概率化的、连续的反应活跃度评估。为什么这件事重要因为传统的代谢分析手段比如GSVA、AUCell、ssGSEA这些本质上是在做基因集的打分。你把一个通路里的基因表达值平均一下或者做个秩和检验得到一个分数。这种做法有两个硬伤第一它忽略了代谢反应之间的拓扑关系一个代谢反应可能涉及多个基因产物这些基因之间的关系不是简单求平均就能刻画的第二它没有考虑代谢网络的整体约束一个反应活跃了它下游的反应大概率也应该活跃这种网络层面的协调性在简单打分里完全丢失了。COMPASS用惩罚回归的方式把代谢网络的结构信息嵌进去了。具体来说它对每个细胞拟合一个回归模型以代谢反应为变量以基因表达为观测值同时加一个基于网络结构的惩罚项使得相邻反应的活跃度倾向于一致。最终每个细胞得到的是一个代谢反应的活跃概率向量你可以拿这个向量去做下游的差异分析、聚类、轨迹推断甚至和细胞类型注释结果做交叉验证。那这个思路适合谁呢如果你已经在做单细胞转录组分析掌握基本的Seurat或者Scanpy流程想做一点超越常规差异基因分析的探索COMPASS是一个很值得尝试的方向。尤其是做肿瘤免疫、自身免疫疾病、感染免疫这些领域的朋友代谢靶点的挖掘往往能给你提供全新的视角。你可能会发现某个看起来“普通”的T细胞亚群在代谢层面有着截然不同的特征而这个特征恰好对应着一个可成药的靶点。接下来我会从整体设计思路、核心细节、实操流程、常见问题几个维度把COMPASS的完整使用路径拆开讲清楚。不是照搬官方文档而是把我自己踩过的坑、调过的参数、验证过的技巧都揉进去让你拿到手就能跑跑完就能用。2. COMPASS到底怎么工作的从设计思路到核心机制2.1 为什么不用简单的通路打分惩罚回归的直觉解释要理解COMPASS为什么用惩罚回归先得理解它要解决的核心问题是什么。假设你有一个细胞测到了20000个基因的表达值。同时你有一个代谢网络模型里面定义了大约13000个代谢反应RECON2的规模每个反应对应一组基因基因-反应关联GPR规则。你的目标是根据这20000个基因的表达值推断这13000个反应中哪些是活跃的。最直接的想法是对每个反应找到它对应的基因看这些基因的表达量。如果基因表达高反应就活跃表达低反应就不活跃。但问题在于一个反应可能对应多个基因而且基因之间的关系可能是“与”也可能是“或”。比如一个酶复合物由三个亚基组成三个基因都表达才能形成有功能的酶这是“与”的关系而如果两个不同的基因都能催化同一个反应同工酶那只要其中一个表达就够了这是“或”的关系。简单的平均或者取最大值都无法准确刻画这种逻辑。更麻烦的是基因表达和代谢反应活跃度之间不是线性关系。一个基因表达量翻倍对应的代谢通量不一定翻倍可能只增加20%也可能因为反馈调节反而下降。而且代谢网络本身有很强的鲁棒性一个反应被抑制了旁路途径可能会代偿性上调。COMPASS的做法是不试图精确建模每个反应的绝对通量而是估计一个“活跃概率”。它把这个问题转化为一个回归问题用代谢反应的活跃度去解释观测到的基因表达值。具体来说对于每个细胞它求解一个优化问题目标函数 拟合误差 惩罚项拟合误差衡量的是如果代谢反应按照某个活跃度分布那么预测的基因表达值和实际观测值之间的差距有多大。惩罚项则包含两部分一是L1惩罚类似Lasso让不活跃的反应活跃度趋向于零保证稀疏性二是网络惩罚让代谢网络中相邻的反应活跃度趋于一致保证平滑性。这个设计的好处是它同时利用了基因表达数据和代谢网络结构信息。L1惩罚让结果可解释你不会得到一个所有反应都中等活跃的模糊答案网络惩罚让结果符合生物学直觉不会出现一个反应活跃但它的底物供应反应完全不活跃这种矛盾情况。2.2 输入数据准备你需要什么样的表达矩阵COMPASS对输入数据有比较具体的要求不是随便扔一个表达矩阵进去就能跑的。首先基因标识符必须是Entrez ID或者Ensembl ID因为代谢网络模型RECON2是用这些ID定义的。如果你用的是基因Symbol需要先做转换。我一般推荐用Ensembl ID因为Symbol在不同数据库版本之间经常变Ensembl ID更稳定。转换可以用biomaRt或者clusterProfiler里的bitr函数但要注意版本匹配问题。其次表达矩阵需要做适当的归一化。COMPASS内部会对数据进行标准化处理但你最好先做log-normalization。如果是Seurat对象用NormalizeData()之后取data槽的数据如果是Scanpy对象用sc.pp.normalize_total()和sc.pp.log1p()处理后的矩阵。原始counts直接扔进去效果通常不好因为不同细胞的测序深度差异会引入很大的噪声。第三建议先做基因过滤。不是所有基因都需要输入COMPASS只保留在代谢网络中有对应的基因即可。RECON2大约涉及2000-3000个代谢相关基因你把这部分基因筛出来矩阵会小很多计算速度也会快很多。具体做法是加载COMPASS自带的代谢网络数据提取所有反应关联的基因列表然后和你的表达矩阵取交集。还有一个容易被忽略的点细胞数量。COMPASS对每个细胞独立拟合模型所以细胞数越多计算时间线性增长。如果你有10万个细胞全跑一遍可能要几个小时甚至更久。我的建议是如果只是探索性分析可以先对每个细胞亚群随机抽样500-1000个细胞跑一遍看看整体趋势确定有信号之后再跑全量数据。2.3 代谢网络模型的选择与配置COMPASS默认使用的是RECON2代谢网络模型这是一个经过人工审校的、覆盖人类代谢主要通路的重构模型。它包含大约13000个反应、7000多个代谢物、2000多个代谢基因。这个模型的好处是覆盖面广、注释详细但缺点是规模大计算量大。如果你做的是小鼠数据需要注意RECON2是人类的模型小鼠基因需要先转换成人类同源基因。可以用babelgene或者homologene包做转换。转换率大概在80%左右会损失一部分基因但主要代谢通路基本都能覆盖。COMPASS还支持自定义代谢网络。如果你只关注某几条特定通路比如糖酵解、氧化磷酸化、脂肪酸氧化可以自己构建一个子网络这样计算速度会快很多结果也更聚焦。构建子网络的方法是从RECON2中提取你感兴趣的反应及其关联基因重新组装成一个小的网络模型。这个操作需要一定的代谢生物学知识但如果你有明确的假设这样做效率最高。配置方面COMPASS有几个关键参数需要调整lambda控制L1惩罚的强度。值越大结果越稀疏只有最强的信号会被保留。默认值通常能用但如果你的数据噪声大可以适当调大。network_penalty控制网络平滑惩罚的强度。值越大相邻反应的活跃度越趋于一致。如果你希望结果更符合网络结构可以调大如果你希望保留更多独立信号可以调小。n_cores并行计算的核数。COMPASS支持多核并行建议设置为可用核数的70%-80%留一些给系统。这些参数没有绝对的最优值需要根据你的数据特点做调整。我的经验是先用默认参数跑一遍看看结果的稀疏程度和生物学合理性然后再微调。3. 从原始数据到代谢靶点完整实操流程拆解3.1 环境搭建与依赖安装COMPASS是一个R包安装方式比较直接但依赖比较多建议用conda创建一个独立环境避免和现有的R环境冲突。conda create -n compass_env r-base4.2 conda activate compass_env进入R之后安装COMPASSinstall.packages(devtools) devtools::install_github(YosefLab/COMPASS)COMPASS依赖几个比较重的包包括Rcpp、RcppArmadillo用于加速矩阵运算、ggplot2用于可视化、pheatmap用于热图绘制。安装过程可能需要几分钟耐心等待。有一个坑需要注意COMPASS对R版本有要求建议用R 4.0以上。如果你用的是R 3.x可能会遇到编译错误。另外Windows系统下安装RcppArmadillo可能需要先安装Rtools版本要和R版本匹配。安装完成后加载COMPASS并检查是否正常library(COMPASS) data(compass_settings)如果能看到compass_settings这个对象说明安装成功。3.2 数据预处理从Seurat对象到COMPASS输入假设你已经有了一个做完标准流程的Seurat对象命名为seurat_obj。第一步是提取表达矩阵并做基因ID转换。# 提取归一化后的表达矩阵 expr_matrix - as.matrix(seurat_objassays$RNAdata) # 假设你的基因名是Symbol需要转换成Entrez ID library(clusterProfiler) library(org.Hs.eg.db) gene_df - bitr(rownames(expr_matrix), fromType SYMBOL, toType ENTREZID, OrgDb org.Hs.eg.db) # 保留能转换的基因 expr_matrix - expr_matrix[gene_df$SYMBOL, ] rownames(expr_matrix) - gene_df$ENTREZID如果你的基因名已经是Entrez ID这一步可以跳过。如果是Ensembl ID用fromType ENSEMBL。接下来加载COMPASS自带的代谢网络数据提取代谢相关基因# 加载RECON2网络 data(recon2_network) # 提取网络中涉及的所有基因 metabolic_genes - unique(unlist(recon2_network$gene_ids)) # 和表达矩阵取交集 common_genes - intersect(rownames(expr_matrix), metabolic_genes) expr_matrix_filtered - expr_matrix[common_genes, ]这一步之后你的矩阵大概会从20000个基因降到2000-3000个基因计算量大幅减少。还有一个重要步骤检查数据分布。COMPASS对数据的分布有一定假设建议先看一下基因表达值的分布情况# 查看表达值分布 hist(expr_matrix_filtered, breaks 100, main Expression Distribution)理想情况下数据应该近似正态分布或者至少是单峰的。如果发现大量零值dropout可以考虑用一些插补方法先处理但注意插补会引入偏差要谨慎。3.3 运行COMPASS参数设置与计算过程数据准备好之后就可以运行COMPASS了。核心函数是COMPASSlibrary(COMPASS) # 设置参数 compass_result - COMPASS( data expr_matrix_filtered, network recon2_network, lambda 0.1, network_penalty 0.05, n_cores 8, verbose TRUE )这个函数会返回一个COMPASSResult对象里面包含了每个细胞在每个代谢反应上的活跃概率。计算时间取决于细胞数和核数。以1000个细胞、8核为例大概需要10-15分钟。如果是10000个细胞可能需要1-2小时。建议在服务器上跑本地机器可能会卡。运行过程中verbose TRUE会输出进度信息你可以看到当前处理到第几个细胞。如果发现速度异常慢可能是某个细胞的数据有问题比如表达值全为零可以先把这类细胞过滤掉。运行完成后提取结果矩阵# 提取代谢反应活跃度矩阵 metabolic_activity - compass_resultmetabolic_activity # 查看维度 dim(metabolic_activity) # 应该是 反应数 x 细胞数这个矩阵就是后续所有分析的基础。每一列是一个细胞每一行是一个代谢反应值在0到1之间表示该反应在该细胞中的活跃概率。3.4 差异代谢分析找到组间显著变化的代谢反应拿到代谢活性矩阵之后下一步就是做差异分析。这里的“差异”可以是组间差异比如治疗组vs对照组也可以是细胞类型间的差异比如耗竭T细胞vs记忆T细胞。我一般用Wilcoxon秩和检验因为代谢活性数据不一定是正态分布非参数检验更稳健# 假设你有分组信息 group - seurat_obj$group # 比如treatment和control # 对每个代谢反应做检验 diff_results - apply(metabolic_activity, 1, function(x) { test - wilcox.test(x ~ group) return(c(p_value test$p.value, mean_treatment mean(x[group treatment]), mean_control mean(x[group control]))) }) # 整理结果 diff_df - as.data.frame(t(diff_results)) diff_df$reaction_id - rownames(diff_df) diff_df$fdr - p.adjust(diff_df$p_value, method BH) # 筛选显著差异的代谢反应 sig_reactions - diff_df[diff_df$fdr 0.05 abs(diff_df$mean_treatment - diff_df$mean_control) 0.1, ]这里有两个阈值FDR 0.05控制假阳性率效应量差异 0.1保证生物学意义。效应量阈值可以根据你的数据调整如果信号弱可以放宽到0.05如果信号强可以收紧到0.2。筛选出显著反应之后需要把这些反应映射回代谢通路看看它们集中在哪些通路上。COMPASS自带的网络数据里有反应-通路的对应关系# 提取反应对应的通路 reaction_pathways - recon2_network$reaction_pathways sig_pathways - reaction_pathways[reaction_pathways$reaction_id %in% sig_reactions$reaction_id, ] # 统计通路富集 pathway_counts - table(sig_pathways$pathway) pathway_counts - sort(pathway_counts, decreasing TRUE)这样你就能看到哪些代谢通路在组间发生了显著变化。比如你可能发现氧化磷酸化通路上有大量反应显著下调而糖酵解通路上有大量反应显著上调这就提示了一个代谢重编程的现象。3.5 可视化让代谢靶点一目了然差异分析的结果需要可视化才能更好地传达信息。我常用的几种图热图展示显著差异的代谢反应在单细胞层面的分布。library(pheatmap) # 选取显著反应 sig_matrix - metabolic_activity[rownames(sig_reactions), ] # 按分组排序 order_idx - order(group) pheatmap(sig_matrix[, order_idx], cluster_cols FALSE, show_colnames FALSE, annotation_col data.frame(group group[order_idx]), main Differential Metabolic Reactions)箱线图展示单个代谢反应在组间的分布差异。library(ggplot2) # 选取top1显著反应 top_reaction - sig_reactions$reaction_id[1] plot_df - data.frame( activity metabolic_activity[top_reaction, ], group group ) ggplot(plot_df, aes(x group, y activity, fill group)) geom_boxplot() theme_minimal() labs(title paste(Reaction:, top_reaction), y Metabolic Activity)火山图展示所有代谢反应的差异倍数和显著性。ggplot(diff_df, aes(x mean_treatment - mean_control, y -log10(fdr))) geom_point(aes(color fdr 0.05 abs(mean_treatment - mean_control) 0.1)) theme_minimal() labs(x Activity Difference, y -log10(FDR))这些图可以帮你快速定位到最有生物学意义的代谢靶点。4. 实操中绕不开的坑常见问题与排查技巧4.1 运行报错与性能问题问题一安装时RcppArmadillo编译失败这是最常见的问题尤其是在Windows系统上。解决方法确保Rtools已安装且版本匹配然后在R中运行Sys.setenv(MAKEFLAGS -j4) install.packages(RcppArmadillo)如果还是不行可以尝试用conda安装conda install -c conda-forge r-rcpparmadillo问题二运行COMPASS时内存溢出COMPASS对每个细胞独立拟合模型但如果细胞数太多中间结果会占用大量内存。解决方法分批处理。把细胞分成若干批每批1000-2000个分别跑COMPASS最后合并结果。# 分批处理 batch_size - 1000 n_batches - ceiling(ncol(expr_matrix_filtered) / batch_size) results_list - list() for (i in 1:n_batches) { start_idx - (i - 1) * batch_size 1 end_idx - min(i * batch_size, ncol(expr_matrix_filtered)) batch_data - expr_matrix_filtered[, start_idx:end_idx] batch_result - COMPASS(batch_data, recon2_network, ...) results_list[[i]] - batch_resultmetabolic_activity } # 合并 metabolic_activity - do.call(cbind, results_list)问题三运行速度太慢除了减少细胞数和增加核数还有一个技巧预筛选代谢反应。如果你只关注特定通路可以在运行前把网络模型缩小到只包含这些通路计算量会大幅减少。# 只保留氧化磷酸化通路 oxphos_reactions - recon2_network$reactions[grep(Oxidative phosphorylation, recon2_network$reactions$pathway), ] sub_network - recon2_network sub_network$reactions - oxphos_reactions sub_network$gene_ids - unique(unlist(oxphos_reactions$gene_ids))4.2 结果解读中的陷阱陷阱一把活跃概率当成绝对通量COMPASS输出的是活跃概率不是绝对通量。一个反应活跃概率0.8不代表它的通量是0.8只代表它比其他反应更可能处于活跃状态。不要跨数据集直接比较绝对值只做组间或细胞类型间的相对比较。陷阱二忽略细胞类型组成的影响如果你做的是组间差异分析而两组的细胞类型组成不同比如治疗组T细胞比例高对照组B细胞比例高那么差异代谢反应可能只是细胞类型组成的反映而不是真正的治疗效应。解决方法要么在同一个细胞类型内部做比较要么用细胞类型作为协变量做校正。# 在T细胞内部做比较 t_cells - WhichCells(seurat_obj, ident T_cells) t_activity - metabolic_activity[, t_cells] t_group - group[t_cells] # 然后做差异分析陷阱三多重检验校正过于保守代谢反应有上万个做多重检验校正后可能一个显著的都没有。这时候可以考虑用通路水平的聚合分析先把反应按通路聚合然后在通路水平做检验。这样检验次数从上万降到几百校正后更容易有显著结果。# 按通路聚合 pathway_activity - apply(metabolic_activity, 2, function(x) { tapply(x, recon2_network$reaction_pathways$pathway, mean) }) # 然后对通路活性做差异分析4.3 与细胞注释结果的交叉验证COMPASS的结果应该和你的细胞类型注释相互印证。如果你发现某个代谢反应在“耗竭T细胞”中特别活跃而这个反应恰好是氧化磷酸化相关的那就和已知的免疫代谢知识一致耗竭T细胞线粒体功能受损代偿性上调糖酵解。但如果发现一个完全出乎意料的结果比如“记忆T细胞”中脂肪酸氧化异常活跃而你的注释可能有问题那就需要回头检查注释是否准确。交叉验证的方法# 计算每个细胞类型的平均代谢活性 cell_types - unique(seurat_obj$cell_type) type_activity - sapply(cell_types, function(ct) { cells - WhichCells(seurat_obj, ident ct) rowMeans(metabolic_activity[, cells]) }) # 热图展示 pheatmap(type_activity, cluster_rows TRUE, cluster_cols TRUE)如果某个细胞类型的代谢特征和它的已知功能不符可能需要重新审视注释。反过来如果代谢特征高度符合那说明你的注释是可靠的同时代谢分析也提供了额外的验证维度。4.4 从代谢靶点到可成药靶点后续验证思路COMPASS给出的差异代谢反应只是第一步要变成可成药靶点还需要几步验证第一步确认关键酶的表达。差异代谢反应对应的酶其基因表达是否也有差异如果酶表达没有差异但代谢活性有差异可能是翻译后调控或代谢物调控这本身也是一个有趣的发现。第二步代谢物水平验证。如果有条件做代谢组学可以检测关键代谢物的水平。比如发现糖酵解通路活跃可以检测乳酸水平发现脂肪酸氧化活跃可以检测乙酰辅酶A水平。第三步功能实验。用抑制剂或基因敲低干扰关键酶看是否影响细胞功能。比如如果你发现某个代谢反应在调节性T细胞中特别活跃可以用对应的抑制剂处理看是否影响Treg的免疫抑制功能。第四步临床数据关联。如果有临床队列数据可以看关键代谢基因的表达是否和患者预后相关。这一步能把基础发现和临床价值连接起来。我自己的经验是COMPASS最大的价值不在于直接告诉你哪个靶点可成药而在于帮你从海量的单细胞数据中筛选出值得进一步研究的候选通路。它把搜索空间从两万个基因缩小到几十个代谢反应大大提高了后续验证的效率。5. 一些让分析更稳的实战心得5.1 参数调优的经验法则COMPASS的参数没有万能值但有一些经验法则可以参考。lambda的选择如果你希望结果稀疏、只保留最强信号用0.2-0.5如果你希望保留更多信号、做探索性分析用0.05-0.1。我一般先用0.1跑一遍看看显著反应的数量如果太少就调小如果太多就调大。network_penalty的选择这个参数控制网络平滑程度。如果你对代谢网络的结构很有信心用0.1-0.2如果你担心网络模型有错误用0.01-0.05。我通常用0.05在平滑和保真之间取一个平衡。n_cores的设置不要设成全部核数留1-2个核给系统。比如你有16核设成14。另外如果内存有限核数太多反而会因为内存竞争导致速度下降。5.2 数据质量的先行检查在跑COMPASS之前有几个数据质量的检查一定要做测序深度每个细胞的UMI总数是否足够如果中位数低于1000代谢基因的检出率会很低COMPASS结果不可靠。线粒体基因比例如果线粒体基因比例过高20%说明细胞状态不好代谢活性可能失真。代谢基因检出率在代谢相关基因中有多少是零表达的如果超过50%都是零说明数据质量不够需要考虑插补或者换数据。这些检查用Seurat或Scanpy的标准流程就能做不要跳过。5.3 结果的可重复性验证单细胞数据分析有一个通病结果不稳定。同样的数据换一个随机种子聚类结果可能就不一样。COMPASS的结果相对稳定但也不是完全确定性的。我的做法是用不同的随机种子跑3-5次看显著代谢反应的重叠程度。如果重叠度高于80%说明结果稳健如果低于50%说明信号太弱需要重新考虑参数或者数据质量。# 多次运行 set.seed(123) result1 - COMPASS(...) set.seed(456) result2 - COMPASS(...) # 比较显著反应的重叠 sig1 - rownames(diff_df1[diff_df1$fdr 0.05, ]) sig2 - rownames(diff_df2[diff_df2$fdr 0.05, ]) overlap - length(intersect(sig1, sig2)) / length(union(sig1, sig2))这个重叠度就是你的结果稳健性的一个量化指标。5.4 和B细胞受体分析、手动注释的联动如果你同时做了BCR分析或者手动注释可以把这些信息和COMPASS结果整合起来。比如把BCR克隆型信息映射到代谢活性矩阵上看不同克隆型的T细胞/B细胞是否有不同的代谢特征。把手动注释的精细亚群和代谢活性做关联看代谢特征是否能进一步细分亚群。这种多模态的整合分析往往能发现单一分析看不到的生物学现象。比如你可能发现某个BCR克隆型的B细胞在氧化磷酸化通路上特别活跃提示这个克隆型处于激活状态值得进一步追踪。# 整合BCR克隆型信息 clone_info - seurat_obj$clone_id clone_activity - sapply(unique(clone_info), function(cl) { cells - WhichCells(seurat_obj, expression clone_id cl) rowMeans(metabolic_activity[, cells]) })这种分析思路可以扩展到任何细胞层面的元数据样本来源、处理条件、时间点、临床信息等等。COMPASS提供的代谢活性矩阵就像一个新的“特征空间”你可以把任何元数据映射上去寻找关联。5.5 计算资源的合理规划最后说一个实际问题计算资源。COMPASS不是那种能在笔记本电脑上轻松跑完的工具。如果你的数据超过5000个细胞建议用服务器至少32GB内存、8核以上。如果数据超过2万个细胞建议用高性能计算集群内存64GB以上。如果资源有限可以考虑以下策略降采样每个细胞亚群随机抽500个细胞先跑一遍看趋势。分批处理把数据分成小批每批跑完保存结果最后合并。通路聚焦只跑你关心的通路缩小网络规模。这些策略我在实际项目中都用过效果不错。关键是先跑通流程确认有信号再考虑全量分析。代谢靶点的挖掘是一个需要耐心和反复验证的过程COMPASS只是提供了一个起点。真正有价值的发现往往来自于你对生物学问题的深入理解和反复推敲。工具是死的问题是活的把工具用在合适的问题上才能发挥它的最大价值。