开头就得说人话。做过单细胞测序分析的人都知道,拿到一张UMAP图只是万里长征第一步——细胞亚群注释完了,下一步最常问的问题就是:这些细胞之间是怎么变化的?在肿瘤、发育、纤维化这类疾病研究里,你拿到的永远是某个时间点的“静态快照”,但疾病进程本身是动态的,细胞从正常状态逐步转变到病理状态,中间要经过哪些中间态、哪些基因在驱动这种转变,这些才是真正能发好文章、能落到机制上的关键信息。单细胞轨迹分析(也叫拟时序分析,pseudotime analysis)就是干这个的,而monocle3是目前用得最多、生态最完善的R包之一。
这篇文章我会从零开始,把monocle3做疾病研究的完整思路和实操流程捋一遍,包括怎么从Seurat对象转换数据、怎么调参数学轨迹、怎么选根节点、怎么找轨迹相关基因,再把我自己踩过的一些坑和排查经验写出来。适合刚接触轨迹分析、被文档绕晕的人,也适合已经跑通流程但想知道怎么把结果解读到疾病机制里的朋友。整个过程我用一个肿瘤上皮-间质转化(EMT)的例子贯穿始终,这样每个步骤你都知道自己在干什么、为什么这么干。
1. 单细胞轨迹分析在疾病研究中的整体思路
1.1 为什么疾病研究需要“轨迹”这个视角
先说个最基本的逻辑。单细胞测序给你的是成千上万个细胞各自的转录组,每个细胞是一个独立样本,但细胞和细胞之间不是孤立的——它们在疾病过程中往往处于不同的分化阶段、不同的激活状态,或者正沿着某条连续的路径从一种状态滑向另一种状态。
举个最常见的例子,肿瘤里的癌细胞。一个上皮来源的肿瘤,早期细胞还保留着上皮特征,表达E-cadherin(CDH1);到了晚期,很多癌细胞会丢失上皮标记、获得间质特征,表达Vimentin(VIM),这个过程就是EMT。但EMT不是开关,而是一个渐变过程,肿瘤组织里同时存在纯上皮、部分上皮、部分间质、纯间质这几种状态的细胞。如果你只看注释后的细胞簇,你会觉得它们像几个孤立的岛;但实际上它们之间有一条连续的通路,癌细胞正在沿着这条路一步步“变形”。
轨迹分析就是把这些离散的点重新串成一条或几条线。它根据细胞之间转录组的相似程度,推断出细胞状态转变的路径,再给每个细胞计算一个“拟时序值”(pseudotime),代表它在转变过程中所处的相对位置。这样一来,你就能回答很多普通差异分析回答不了的问题:某个致病基因是在转变早期上调还是晚期上调?哪些基因模块先变化、哪些后变化?哪群细胞是转变的起点、哪群是终点?这对于理解疾病机制、寻找早期干预靶点非常有价值。
1.2 monocle3在众多轨迹工具里的定位
轨迹分析的工具很多,Slingshot、SCORPIUS、PAGA、scVelo(RNA velocity)各有各的长处,但我个人在疾病研究场景里用得最多的还是monocle3,理由有三条:
第一,monocle3的学习目标是“主图”(principal graph),相当于在细胞分布里拟合出一条可以分叉、可以成环的骨架。这个骨架能很好地表达“从一个状态分化成多个方向”这类生物学事件,比如造血干祖细胞向下游多个谱系分化,或者肿瘤细胞一部分走向EMT、一部分走向增殖,这些都能可视化得很清楚。
第二,monocle3在确定细胞轨迹后,自带一套完整的差异分析、基因模块分析工具,不需要你再切换到别的R包去折腾。graph_test、find_gene_modules这些函数用起来很顺手,能从轨迹里直接找到随拟时序变化的基因,再聚成模块做富集,整个下游链路是闭环的。
第三,monocle3的设计从单细胞数据出发,底层用UMAP降维、Leiden聚类,处理十万级细胞也没太大压力。而且它支持把不同样本、不同批次的细胞放进一个数据集里做分析,用align参数校正批次效应,这对疾病研究里常见的“正常组+疾病组多例样本合并”场景非常友好。
当然,monocle3也有它的短板:UMAP本身有一定随机性,不同seed跑出来的轨迹可能略有差异;轨迹推断是计算推测,不等于真实的谱系追踪,结论必须要结合实验或者至少结合RNA velocity做交叉验证。这些后文会详细说怎么处理。
2. 从Seurat对象到monocle3的数据准备工作
2.1 安装和环境配置
如果是从零开始,先确认你的R版本在4.0以上,建议R 4.2或者4.3。monocle3对依赖包的要求比较杂,包括BiocManager安装的一部分Bioconductor包,以及一些GitHub上的开发版包。推荐用下面这套顺序装,能避开大部分依赖报错:
# 先安装BiocManager(如果还没有) if (!requireNamespace("BiocManager", quietly = TRUE)) { install.packages("BiocManager") } # 安装monocle3所需的Bioconductor依赖 BiocManager::install(c("BiocGenerics", "DelayedArray", "DelayedMatrixStats", "S4Vectors", "SingleCellExperiment", "SummarizedExperiment", "batchelor", "Matrix", "Rcpp", "RcppHNSW", "irlba")) # 安装monocle3本体(来自Trapnell实验室的GitHub) install.packages("devtools") devtools::install_github("cole-trapnell-lab/monocle3")安装过程中最容易出问题的是RcppHNSW和batchelor,前者在部分Linux服务器上需要系统有较新的gcc,后者需要正确安装BiocManager版本。如果报错,优先看是不是缺系统依赖,比如libgdal、libgeos之类,用apt或者yum装上再重试。
装好之后,加载时如果报“package 'monocle3' was built under R version xxx”的警告,一般不影响使用;但如果报错提示找不到某个函数,比如preprocess_cds不存在,先检查是不是装了monocle2而不是monocle3——这两个包的API差别非常大,很多新手在这里被坑过。可以用packageVersion("monocle3")确认版本号,正常应该是1.x。
2.2 从Seurat对象转换为cell_data_set
绝大多数人做单细胞上游分析用的是Seurat,做完标准化、降维、聚类、注释之后,再切到monocle3做轨迹。这里有一个非常关键的点:monocle3最标准的做法,不是把原始count矩阵丢给它重新走一遍流程,而是直接把Seurat里已经降维好的UMAP坐标和聚类结果转过去。
为什么?因为你在Seurat里已经做了仔细的QC、批次整合(比如Harmony或者CCA)、聚类粒度调整,这些前期投入不应该浪费;而且monocle3的learn_graph是在UMAP空间里做主图学习,直接用你熟悉的、已经注释好的UMAP,后续解读轨迹和细胞簇身份时,心智负担会小很多。
转换代码很简单:
library(Seurat) library(monocle3) # 假设你有一个已经做完注释的Seurat对象:seu # 关键:确保seu里有UMAP坐标和Cluster信息 cds <- as.cell_data_set(seu) # 把Seurat里的UMAP坐标赋值给cds cds@reduce_dim_aux[["UMAP"]] <- NULL reducedDims(cds) <- list(UMAP = seu@reductions$umap@cell.embeddings) # 把Seurat的聚类结果赋给cds cds@clusters[["UMAP"]] <- list( cluster_result = NULL, partitions = seu@meta.data$seurat_clusters, clusters = seu@meta.data$seurat_clusters ) # 建议把细胞类型注释也存进来,方便后面看图 cds@colData@listData[["cell_type"]] <- as.character(seu@meta.data$cell_type)这里有个小坑:as.cell_data_set(seu)转换后,cds里的UMAP坐标默认可能是空的,所以需要手动从seu里拉出来塞回去。另外,cds@clusters[["UMAP"]]这个list结构必须包含partitions和clusters两个名字,partitions表示大的分区(通常就是不同谱系),clusters表示更细的细胞簇。如果你只有一级注释,两者赋成一样的东西问题不大,后面还可以再跑一次monocle3自己的聚类来细分。
如果你没有现成的Seurat对象,也可以从count矩阵直接构建cds,然后走monocle3自己的QC和标准化流程,但这意味着前期细致的细胞注释工作要在monocle3里重做,效率低很多,我不建议。实际项目里几乎都是先Seurat后monocle3这个路径。
3. monocle3完整分析流程与核心参数
3.1 预处理与降维
数据转换好之后,正式分析的第一步是preprocess_cds。这个函数做的事情本质上和Seurat里的ScaleData+RunPCA类似,但它内置了自己的参数和逻辑,用来为后续的UMAP降维做铺垫。
cds <- preprocess_cds(cds, num_dim = 50, method = "PCA")先解释一下num_dim。这个参数控制你保留多少个主成分作为后续UMAP的输入。我的习惯是先看Seurat里ElbowPlot的拐点,如果拐点不明显,就直接用50。注意,preprocess_cds这一步会重新计算PCA,所以它在某些情况下可能和你Seurat里已有的PCA结果有差异,但不要紧——只要UMAP坐标我们已经从Seurat带过来了,后面轨迹学习主要依赖的是UMAP空间,preprocess_cds更多是补全内部数据结构和表达矩阵的行列信息。
降维这一步用reduce_dimension:
cds <- reduce_dimension(cds, reduction_method = "UMAP", preprocess_method = "PCA")默认情况下它会在cds内部重新算一遍UMAP,覆盖掉我们之前从Seurat带过来的坐标。如果你想让monocle3用Seurat的UMAP结果,可以在参数里加umap.fast = TRUE然后不传坐标,但更省事的做法是直接接受monocle3自己算的这个UMAP,因为后续learn_graph和可视化都是基于这个内部UMAP的。为了确保可重复性,这一步一定要设置随机种子,比如set.seed(2024),否则不同次运行轨迹可能漂移。
3.2 聚类与分区(partition)的生物学意义
降维完就是聚类。monocle3默认用Leiden算法,这是目前公认在单细胞聚类里速度和效果比较均衡的算法。
cds <- cluster_cells(cds, resolution = 1e-3)resolution是Leiden算法里控制聚类粒度的参数,默认1e-3。如果你发现聚类粒度过粗或者过细,可以调大或者调小。这里要特别强调的是cluster_cells会同时产生两个层级的结果:clusters和partitions。partitions是把差异特别大的细胞群分开的大分区,比如一个数据集里同时有免疫细胞和上皮细胞,大概率会被分成两个分区;clusters则是在分区内部更细的亚群。
为什么分区这么重要?因为monocle3的轨迹学习原则是“每个分区内部学一条主图”,也就是说,如果某两个细胞类型被算法分到了不同partition,它们就不会被强行走同一条轨迹。这对疾病研究来说非常合理——你不能把T细胞和上皮细胞硬拉到一个分化轨迹里,那是没有生物学意义的。但如果你的研究问题恰恰是跨谱系转变,比如上皮细胞转分化成间质细胞本身形成了一个连续过渡,那你可能需要调大resolution让它们落进同一个分区,或者手动合并分区,这个后面会讲。
聚类完成后,建议先用plot_cells快速检查一下分区和聚类的分布,确认和你在Seurat里注释的细胞类型是否对应,如果明显错位,回头检查是不是UMAP坐标赋值出了问题。
3.3 学习轨迹主图:learn_graph的原理与调参
接下来是最核心的一步——learn_graph,也就是在UMAP空间里学习一条主图。
cds <- learn_graph(cds, use_partition = TRUE)use_partition = TRUE表示每个partition分别学习轨迹。这一步背后的算法是:先在细胞分布上构建一个最近邻图,然后在此基础上寻找一条能够经过数据主体区域、同时保留主要分叉节点的曲线骨架。这个过程类似用一根可弯曲的金属丝去“穿过”一团棉花糖,金属丝的形态就是轨迹骨架。
这一步骤中,我最常遇到的问题是主图节点(principal graph nodes)过少或过多。节点太少,轨迹会过于简化,把真实存在的分支抹掉了;节点太多,轨迹会过度拟合,出现很多琐碎的小分叉,反而不好解读。monocle3里有一个隐藏参数learn_graph_control,里面可以调ncenter或者geodesic_neighbor_number等,但默认参数在大多数数据集上表现都还行,我建议先跑默认,再根据可视化结果决定是否调整。
如果learn_graph跑完后,轨迹图上有一些“飞出去”的孤立短枝,很多时候是少数离群细胞导致的,可以在learn_graph之前先用choose_graph_segments清理一下,或者直接在umap坐标层面把离群细胞过滤掉。这个属于细活,等到后面“常见问题”部分细说。
3.4 根节点选择决定拟时序的解释方向
轨迹学完,紧接着是order_cells——这一步决定整个拟时序分析的成败。order_cells需要你指定一个“根节点”(root node),也就是轨迹的起点,然后monocle3会沿着主图给每个细胞计算拟时序值。
# 先可视化,手动找到根节点编号 plot_cells(cds, color_cells_by = "pseudotime", label_groups_by_cluster = TRUE, label_leaves = TRUE) # 运行交互式选择根节点 cds <- order_cells(cds, reduction_method = "UMAP")运行order_cells(cds)之后,R会弹出一个图窗口,你可以点击主图上的某个节点位置设定根。如果是在服务器上跑,没有图形界面,可以用下面的方式在代码里指定根节点:
# 先获取主图节点列表 # 然后根据节点编号设置根 cds <- order_cells(cds, root_prune_graph = TRUE, root_cells = colnames(cds[, clusters(cds) == "某个根细胞簇"]))order_cells支持两种指定方式:一种是指定root_cells,即给出一批细胞作为轨迹起点;另一种是交互式点击root_nodes。实际项目中我更喜欢用root_cells——因为我可以根据已有的生物学知识,明确指定“这簇注释为正常上皮的细胞是EMT的起点”,这比在图里手动点一个节点更可解释、也更好复现。
根节点选错,拟时序结果会完全变样。比如EMT轨迹里,如果你把间质态细胞选为根,那拟时序就会从间质往上皮方向跑,下游所有基因模块的解读方向就反了。所以选根之前,务必回到你的细胞注释和marker表达上,确认哪群细胞是“最初始”的状态。对肿瘤EMT这个例子,正常、非转化或上皮特征最明显的细胞簇就是合理的起点。
4. 下游分析:从轨迹到疾病机制的解读
4.1 用graph_test找到随拟时序变化的基因
轨迹构建好了,拟时序也算了,接下来才是真正和疾病研究结合的部分。第一个常用操作是用graph_test检测哪些基因的表达显著沿着轨迹变化——这才是你发文章时“轨迹分析发现XX基因动态变化”的依据来源。
# 检测沿轨迹变化的基因 gene_fits <- graph_test(cds, neighbor_graph = "principal_graph", cores = 4) # 查看结果 head(gene_fits[order(gene_fits$q_value), ])graph_test的输出里,最关键的两列是morans_I和q_value。morans_I是莫兰指数,衡量基因表达在轨迹空间里的空间自相关性,值越大说明基因表达越规律地沿着轨迹变化;q_value是校正后的显著性。实际筛选时,一般取q_value < 0.05且morans_I > 0.25的基因作为候选轨迹相关基因。注意,morans_I的阈值没有绝对标准,要根据你的数据分布看,有些数据集0.15就能筛出很多有意义基因,有些则需要0.3以上。先跑一遍把基因按morans_I从高到低排,我看一下排序里前20个基因是不是marker,大概就能判断该卡多少。
筛出来的基因可以画热图,直观展示它们在拟时序上的表达趋势:
# 挑top显著基因 top_genes <- gene_fits %>% filter(q_value < 0.05) %>% arrange(desc(morans_I)) %>% pull(gene_short_name) %>% head(30) # 提取它们的表达数据并绘制热图 plot_genes_in_pseudotime(cds[top_genes, ], color_cells_by = "cell_type", min_expr = 0.5)这个热图能非常清晰地看出基因表达的先后顺序,比如EMT研究里,EPCAM、CDH1这类上皮基因在拟时序早期高表达,随后下降;VIM、FN1、ZEB1这类间质基因在后期上升。这种动态趋势本身就是疾病机制的直接证据。
4.2 用find_gene_modules聚合基因模块
单独看几百个基因很累,这时候用find_gene_modules把所有轨迹相关基因聚成模块,每个模块代表一组协同变化的基因,然后对每个模块做GO/KEGG富集。
# 提取所有显著基因构建子集 pr_graph_test_res <- gene_fits pr_genes <- row.names(subset(pr_graph_test_res, q_value < 0.05)) # 构建基因模块 gene_module_df <- find_gene_modules(cds[pr_genes, ], resolution = 0.001) # 按模块画表达热图 plot_cells(cds, genes = gene_module_df, label_cell_groups = FALSE, show_trajectory_graph = FALSE)find_gene_modules内部也是用Leiden聚类把基因分组,resolution控制分组的细致程度。模块分完之后,每个模块对应一个“表达随时间变化的基因程序”。你可以把模块导出,用clusterProfiler做富集分析,看哪些通路在轨迹早期激活,哪些在晚期激活。这个结果对解释疾病机制非常有力:比如EMT过程中,模块A富集到“细胞黏附”和“上皮发育”,拟时序早期活跃;模块B富集到“细胞迁移”“细胞外基质组织”,晚期活跃;这就构成了一个完整的“上皮特征丢失-间质特征获得”的分子叙事。
4.3 结合细胞类型和疾病分组解读轨迹
单纯看基因还不够,疾病研究里更常问的问题是:正常细胞和疾病细胞在轨迹上的分布有没有差异?比如EMT轨迹里,正常组织来源的上皮细胞是不是集中在早期拟时序段,而肿瘤组织来源的细胞是不是分布在后段?这可以直接用拟时序值做分组比较:
# 提取拟时序值 pseudo_df <- data.frame( pseudotime = pseudotime(cds), cell_type = cds@colData$cell_type, group = cds@colData$sample_group ) # 用ggplot2画分组密度分布 ggplot(pseudo_df, aes(x = pseudotime, fill = group)) + geom_density(alpha = 0.5) + theme_classic()如果两组细胞的拟时序分布有明显偏移,说明疾病状态下更多细胞处于轨迹的“晚期”状态,也就是疾病推动了细胞沿这条转变路径前进。这类图在实际文章里非常常见,审稿人也容易接受。
还有一个常用技巧是计算每个样本的“平均拟时序”(mean pseudotime),然后和临床指标做相关分析。比如在肿瘤里,平均拟时序越高的样本,生存期越短,这就能把单细胞轨迹和临床预后关联起来,研究档次一下就上去了。当然,做这种分析时要注意样本量,单细胞数据来自几个病人的话,谨慎下结论,最好用bulk RNA-seq的大队列做验证。
5. 常见问题与排查技巧实录
5.1 我在实际项目中踩过的五个坑
第一个坑:UMAP坐标错位导致轨迹断裂。我刚开始用as.cell_data_set转Seurat对象时,没有重新赋值UMAP坐标,结果learn_graph画出来的轨迹乱成一团,有些细胞被孤立在主图外。排查了半天发现是reducedDims(cds)里的坐标是空的或者和meta不对应。解决办法就是我前面写的,转换后必须手动reducedDims(cds) <- list(UMAP = seu@reductions$umap@cell.embeddings),然后把clusters也一并赋好。
第二个坑:分区太多导致轨迹分散。有一个免疫细胞数据集,CD4和CD8 T细胞明明在UMAP上接近,但cluster_cells把它们分到了两个partition,learn_graph结果就成了两条互不相干的轨迹。这时候如果想看它们共同的活化轨迹,可以在learn_graph里设置use_partition = FALSE,或者在cluster_cells之前把resolution调大,让它们别被拆开。
第三个坑:根节点选错,整个方向反转。我在一个纤维化例子里,一开始想当然选了成纤维细胞作为起点,结果拟时序把炎症细胞放在“后期”,和已知病理过程完全相反。后来改回损伤初始状态的上皮细胞作为根,轨迹才合理。记住,根节点决定一切,不是随便点一个位置就算完。
第四个坑:graph_test跑起来特别慢。几十万细胞的数据集,graph_test默认会比较耗内存。解决办法一个是设置cores参数做并行,另一个是提前过滤低表达基因(比如在至少10个细胞里表达量大于0.1),把输入基因数降下来。
第五个坑:monocle3版本的函数名和教程对不上。网上很多教程是旧版monocle3的写法,比如partition_cells这个函数现在已经不推荐直接用了。遇到函数找不到或者参数报错,先看packageVersion("monocle3"),再查对应版本的官方文档,不要死磕旧代码。
5.2 如何验证轨迹的可靠性
我觉得这是整个分析里最容易被忽视、但其实最重要的问题。轨迹分析本质是计算推断,你用UMAP结构推出来的“拟时序”不代表真实时间轴,必须要做验证。
第一个验证手段是RNA velocity。用velocyto或者scVelo估算每个细胞的转录本剪接动态,得到“细胞正在向哪个方向转变”的速度场,然后把速度场和monocle3的轨迹叠加看是否一致。如果RNA velocity箭头方向和你轨迹方向吻合,结论就靠谱很多。虽然scVelo要在Python环境里跑,但它的结果可以导出成csv再回到R里可视化,这个跨工具流程不复杂,强烈推荐做。
第二个验证手段是转录因子活性或者蛋白水平的验证。比如轨迹推断T细胞正在从naive向exhausted转变,那就应该看到TOX、PDCD1等关键转录因子和蛋白的表达量变化,如果能结合流式或免疫组化的时间序列数据,说服力更强。
第三个更朴素的验证是marker基因分布的“排列感”。在UMAP图上,如果一个轨迹真的有生物学意义,它的关键marker表达变化应该是渐变的而不是随机斑驳的。我每次学完轨迹,第一件事就是挨个把已知marker画一遍,确认渐变顺序符合文献报告,再做下游分析。
5.3 问题速查表
| 现象 | 可能原因 | 解决办法 |
|---|---|---|
| 轨迹图断裂,细胞散落各处 | UMAP坐标未正确赋值 | 从Seurat重新赋值reducedDims,检查细胞名是否一一对应 |
| learn_graph后出现大量孤立短枝 | 离群细胞过多或分区过细 | 过滤离群细胞,适当调大resolution合并分区 |
| 轨迹分叉难以解释 | 分区切分不当,或者数据里真的存在多谱系 | 检查每组branch对应的marker,考虑分开分析 |
| 根节点找不到想要的细胞簇 | 聚类粒度不匹配 | 手动指定root_cells来代替点击root_nodes |
| graph_test速度极慢 | 基因数太多 | 过滤低表达基因,设置cores并行 |
| 两次运行轨迹结果不同 | UMAP/Leiden的随机性 | set.seed固定随机种子,记录版本号 |
| 转换后细胞类型丢失 | colData未正确继承 | 手动把meta.data里的列赋值给cds@colData |
6. 工具选型解析:何时必须用monocle3
6.1 几个主流轨迹工具的横向对比
| 工具 | 原理 | 优势 | 局限 | 适合场景 |
|---|---|---|---|---|
| monocle3 | 主图学习(principal graph) | 可视化好、内置基因模块、支持复杂分叉 | 运行较慢,UMAP随机性 | 疾病分期、分化轨迹、EMT等连续渐变 |
| monocle2(DDRTree) | 反向图嵌入 | 对简单线性轨迹稳定、经典 | 处理不了复杂分叉,数据量大很慢 | 早期数据、简单分化轨迹的复现 |
| Slingshot | 利用聚类结果构建最小生成树 | 速度快、接口灵活、能接入Seurat | 轨迹形状依赖聚类质量 | 已有良好聚类结果,想快速出轨迹 |
| scVelo(RNA velocity) | 剪接动力学模型 | 真正反映细胞转变方向、不依赖根选择 | 需要Python环境、对数据质量要求高 | 验证拟时序方向、推断短期细胞命运 |
| PAGA | 基于分区和连接的可解释图 | 超大数据集下速度优势明显 | 轨迹不是连续曲线,是拓扑图 | 大规模图谱项目(如人类细胞图谱) |
看到这个表格你可能发现了,工具之间不是互斥关系,而是互补关系。在我实际项目里,monocle3往往是主力工具,负责产出轨迹曲线和拟时序排序;scVelo做方向验证;Slingshot偶尔用来做敏感性分析——如果换个算法还能得出类似的轨迹结构,那就说明结论是稳健的。
6.2 我的选择逻辑
如果是以下场景,我优先用monocle3:
- 你已经有画好的UMAP和细胞注释,想快速产出轨迹图。
- 你的研究问题是连续的细胞状态转变,比如EMT、巨噬细胞极化、T细胞耗竭、纤维化进展。
- 你希望一套代码搞定轨迹、差异基因、基因模块、富集分析。
- 你的项目里需要跨样本比较拟时序分布,比如疾病组和对照组的细胞在轨迹上位置不同。
如果是以下场景,我会考虑换工具或者做补充:
- 你关注的是细胞“真实分化方向”而不是抽象拟时序——这时候RNA velocity是必须的。
- 你的数据形状非常复杂,比如多个发育分支同时存在,PAGA的拓扑结构可能比连续主图更好展示。
- 你只想快速验证一个分化方向,Slingshot可能半小时就能出结果。
7. 从分析到文章:结果呈现和叙事技巧
7.1 主图的呈现方式
轨迹分析最终要变成文章里的图。我用monocle3出图时,一般会保存两种风格:
第一种是UMAP叠加主图,用plot_cells(cds, color_cells_by = "cell_type", label_groups_by_cluster = TRUE),这是所有论文里最常见的形式,用来展示不同细胞类型在轨迹上的分布。
第二种是UMAP叠加伪时序的连续色标图,用plot_cells(cds, color_cells_by = "pseudotime")。我通常会把颜色调成从蓝到红渐变,并在图注里写清楚“蓝色代表轨迹早期,红色代表轨迹晚期”。
第三种是对关键基因的“双联图”——左边是UMAP上该基因的表达量,右边是拟时序热图。这种方式能在同一张图里同时呈现空间位置和时间动态,很有说服力。
7.2 与疾病分组的关联呈现
文章里如果要把疾病和轨迹关联起来,我推荐一个“三件套”:第一张图画两种状态下细胞的拟时序密度分布;第二张图画每个样本的平均拟时序,分组做boxplot;第三张图把平均拟时序和临床指标做相关散点图。这套组合拳能让你从“轨迹存在”进阶到“轨迹变化与疾病相关”,逻辑链完整且容易复现。
当然,做疾病关联时样本分组设计一定要提前规划好。如果你想比较“正常 vs 疾病”的拟时序差异,最少要有3个生物学重复,单细胞数据尤其要注意批次效应。如果不同组样本是分批测的,建议在Seurat整合时用Harmony处理,再转到monocle3做轨迹,否则轨迹本身可能反映的是批次差异而不是疾病差异,这是一个非常严重的潜在假阳性来源。
8. 最后再分享一个实用小技巧
流程走多了以后,我有一个习惯,每次跑轨迹分析都会把sessionInfo()连同核心参数存成一个txt,放在项目文件夹里。这不是形式主义——monocle3的版本迭代确实会带来结果变化,两年后如果你要复现这篇文章的分析,没记录版本和参数几乎是灾难。
另外一个小技巧是关于结果保存的。learn_graph和order_cells的结果都在cds对象里,但我强烈建议单独保存拟时序表、模块表和graph_test结果表,而不是只保存整个cds。因为cds文件非常大(几个G),下游做差异分析、画图,每次加载都浪费时间。表格才是你后边真正用得多的东西。
单细胞轨迹分析这条路线,说难是真不难——装好包、跑通流程只需要一天;但说容易也不容易——真正让轨迹分析有生物学价值、能被审稿人认可,靠的是对数据细节的把控、对根节点选择的理解、以及交叉验证的习惯。希望这篇实操笔记能帮你少走一些弯路。