为什么要进行聚类? 在上一节中,我们已经完成了 PCA、邻居图和 UMAP。此时每个细胞已经不再只是一个高维基因表达向量,而是在 PCA 空间和邻居图中有了相对清晰的位置关系。
聚类(clustering)的目的,是在这些细胞之间的相似性关系中,找到表达模式相近的一组细胞。简单理解就是:
细胞邻居图 → 细胞群 在单细胞 RNA-seq 分析中,聚类通常用于帮助我们发现数据中的主要细胞群、细胞状态或潜在亚群。它本身不是最终的细胞类型注释,而是为后续 marker gene 分析和细胞类型注释提供一个分组基础。
需要注意的是,聚类结果不等同于真实细胞类型。一个真实细胞类型可能会因为状态差异、细胞周期、激活状态或技术因素被拆成多个 cluster;多个相近细胞类型也可能在较低分辨率下被合并到同一个 cluster。因此,聚类结果需要结合 marker gene、样本信息、QC 指标和生物学背景进行解释。
聚类的基本思想 在 Scanpy 的常规流程中,聚类不是直接在原始表达矩阵上进行,也不是在 UMAP 二维坐标上进行,而是在邻居图上进行。
上一节降维中的核心步骤是:
sc.pp.neighbors( adata, n_neighbors= 15 , n_pcs= 30 , random_state= 0 ) 这一步会在 PCA 空间中寻找每个细胞的近邻,并构建细胞之间的 KNN graph。可以把这个图理解为一个由细胞构成的网络:
Leiden 聚类算法会在这个邻居图上寻找连接更紧密的细胞社区,也就是我们后续看到的 cluster。
因此需要特别区分:
PCA 空间 → 构建邻居图 → Leiden 聚类 PCA 空间 → 构建邻居图 → UMAP 可视化 UMAP 和 Leiden 都依赖邻居图,但二者用途不同。UMAP 主要用于可视化,Leiden 用于产生聚类标签。不要直接根据 UMAP 二维图上的距离或形状来定义 cluster。
Leiden 和 Louvain Leiden 和 Louvain 都是图聚类算法,常用于在单细胞邻居图上发现细胞群。Louvain 是较早使用的方法,Leiden 是对 Louvain 的改进。
目前单细胞分析中通常优先使用 Leiden。相比 Louvain,Leiden 更稳定,也能减少一个 cluster 内部并不连通的问题。因此在新的 Scanpy 流程中,建议优先使用:
sc.tl.leiden() Louvain 可以作为历史方法或对比方法了解,但一般不作为首选。
读取降维后的数据 import os import pandas as pd
import scanpy as sc adata = sc.read_h5ad( "/home/data/t090639/project/pbmc_10k/data/processed/05_pbmc_10k_dimensionality_reduction.h5ad" ) adata 这里读入的是上一节保存的降维结果。这个对象应该已经包含 PCA、邻居图和 UMAP 结果。
可以先检查对象中是否已经有这些信息:
print(adata.obsm.keys()) print(adata.obsp.keys()) print(adata.uns.keys()) 通常应该能看到:
adata.obsm["X_pca"] :PCA 坐标。 adata.obsm["X_umap"] :UMAP 坐标。 adata.obsp["distances"] :邻居距离矩阵。 adata.obsp["connectivities"] :邻居连接矩阵。 adata.uns["neighbors"] :邻居图参数。 如果当前对象没有邻居图,可以重新运行:
n_pcs =
30 sc.pp.neighbors( adata, n_neighbors= 15 , n_pcs=n_pcs, random_state= 0 ) 如果上一节已经保存了邻居图,这一步可以不用重复运行。
运行 Leiden 聚类 先从一个常用分辨率开始,例如 resolution=0.5 :
sc.tl.leiden( adata, resolution= 0.5 , key_added= "leiden_res0_5" , flavor= "igraph" , n_iterations= 2 , random_state= 0 ) 这里几个参数的含义是:
resolution=0.5 :控制聚类颗粒度。值越高,通常得到的 cluster 越多;值越低,cluster 越少。 key_added="leiden_res0_5" :将聚类结果保存到
adata.obs["leiden_res0_5"] 中,避免覆盖其他分辨率的结果。 flavor="igraph" :使用 igraph 后端运行 Leiden。 n_iterations=2 :运行 2 次优化迭代,是常用且效率较高的设置。 random_state=0 :固定随机种子,方便复现结果。 执行完成后,聚类标签会被写入 adata.obs :
adata.obs[[ "leiden_res0_5" ]].head() 查看每个 cluster 的细胞数:
adata.obs[ "leiden_res0_5" ].value_counts().sort_index() 也可以整理成表格:
cluster_key = "leiden_res0_5" cluster_counts = adata.obs[cluster_key].value_counts().sort_index().to_frame( "n_cells" ) cluster_counts[ "pct_cells" ] = cluster_counts[ "n_cells" ] / adata.n_obs * 100 cluster_counts 如果某个 cluster 细胞数非常少,需要特别谨慎。它可能是真实稀有细胞群,也可能是低质量细胞、doublet、批次效应或聚类分辨率过高造成的小群。
在 UMAP 上查看聚类结果 聚类结果通常会画在 UMAP 上进行观察:
sc.pl.umap( adata, color= "leiden_res0_5" , legend_loc= "on data" ) 这里需要再次强调:Leiden 聚类不是在 UMAP 坐标上运行的。UMAP 只是把聚类标签显示出来,方便我们观察不同 cluster 在二维空间中的分布。
如果想让图例放在图外,可以使用:
sc.pl.umap( adata, color= "leiden_res0_5" , legend_loc= "right margin" ) 尝试不同分辨率 单细胞聚类中通常不会只看一个
resolution 。因为不同分辨率会得到不同颗粒度的细胞群。
低分辨率更适合观察粗略的主要细胞类型;高分辨率更适合发现细胞状态或亚群,但也更容易把噪音拆成小 cluster。
可以一次运行多个分辨率,并把结果保存到不同列中:
resolution_settings = { "leiden_res0_25" : 0.25 , "leiden_res0_5" : 0.5 , "leiden_res0_8" : 0.8 , "leiden_res1" : 1.0 , "leiden_res1_5" : 1.5 , } for key, res in resolution_settings.items(): sc.tl.leiden( adata, resolution=res, key_added=key, flavor= "igraph" , n_iterations= 2 , random_state= 0 ) print(key, adata.obs[key].nunique()) 然后把不同分辨率画在同一张 UMAP 上进行比较:
sc.pl.umap( adata, color=[ "leiden_res0_25" , "leiden_res0_5" , "leiden_res0_8" , "leiden_res1" ], legend_loc= "on data" , ncols= 2 ) 判断分辨率时,不应该只看 cluster 数量,而应该结合后面的 marker gene 和 QC 信息:
如果一个 cluster 中明显混合了两类互斥 marker,可能说明分辨率太低,需要拆开。 如果多个小 cluster 的 marker gene 几乎完全一样,可能说明分辨率太高,可以合并解释。 如果某个小 cluster 主要由高线粒体比例、极端 UMI 或某个批次组成,应优先考虑技术因素。 对于 PBMC 这类细胞类型相对清楚的数据,可以先从 resolution=0.5
或 resolution=0.8 开始。最终使用哪个分辨率,应该在 marker gene 分析和细胞类型注释之后再确定。
检查聚类结果是否被 QC 指标驱动 聚类完成后,第一步不是马上给 cluster 命名,而是检查 cluster 是否主要由技术因素驱动。
先用 QC 指标给 UMAP 上色:
qc_cols = [col for col in [ "total_counts" , "n_genes_by_counts" , "pct_counts_mt" ] if col in adata.obs.columns] if len(qc_cols) > 0 : sc.pl.umap( adata, color=[ "leiden_res0_5" ] + qc_cols, ncols= 2 ) 还可以按 cluster 计算 QC 指标的中位数:
cluster_key = "leiden_res0_5" adata.obs.groupby(cluster_key, observed= True )[ [ "total_counts" , "n_genes_by_counts" , "pct_counts_mt" ] ].median().sort_values( "pct_counts_mt" , ascending= False )
如果某个 cluster 的 pct_counts_mt 明显偏高,或者 total_counts 、 n_genes_by_counts 极端异常,需要考虑它是否是低质量细胞、doublet 或残留技术噪音。
如果对象中有样本或批次信息,也应该检查 cluster 是否被样本或批次主导:
sample_cols = [col for col in [ "sample" , "batch" ] if col in adata.obs.columns] for col in sample_cols: print(col) print(pd.crosstab(adata.obs[cluster_key], adata.obs[col], normalize= "index" )) 如果某个 cluster 几乎只来自一个样本或一个批次,需要结合实验设计判断它是真实生物学差异,还是批次效应。
使用 marker gene 初步检查聚类 对于 PBMC 数据,可以先用一些经典 marker gene 粗略检查聚类是否合理。
marker_genes = [ "MS4A1" , "CD3D" , "CD8A" , "LYZ" , "NKG7" , "PPBP" ] if adata.raw is not None : marker_genes = [gene for gene in marker_genes if gene in adata.raw.var_names] if len(marker_genes) > 0 : sc.pl.umap( adata, color=[ "leiden_res0_5" ] + marker_genes, use_raw= True , ncols= 3 ) 这里使用 use_raw=True ,是为了展示 Log(CP10k+1) 后的表达值,而不是 scaling 后的 z-score。
常见 PBMC marker 可以这样初步理解,但不同的组织样本可能有不同的 marker:
NKG7 :NK cells 或 cytotoxic T cells。 这一步只是初步检查,不等同于正式注释。正式注释通常需要在每个 cluster 中寻找 marker gene,并结合已有细胞类型知识进行判断。
如何选择最终聚类结果? 在实际分析中,常常会保存多个分辨率的 Leiden 结果,但最终注释时需要选择一个主要使用的聚类标签。
可以先选一个中等分辨率作为起点,例如:
cluster_key = "leiden_res0_5" 如果后续 marker gene 分析发现某些 cluster 内部仍然混有明显不同的细胞类型,可以尝试更高分辨率,例如:
cluster_key = "leiden_res0_8" 如果高分辨率拆出了很多 marker gene 几乎相同的小 cluster,则可以回到较低分辨率,或者在注释时把相近 cluster 合并解释。
需要注意的是,
resolution 不是一个越高越好的参数。它控制的是算法划分细胞社区的细度,而不是细胞类型的真实数量。
亚群再聚类(可选) 有时我们会对某一个大类细胞进行更细致的分析。例如,在完成全局聚类和初步注释后,只取出 T cells,再重新进行邻居图、UMAP 和 Leiden 聚类,以寻找 T cell 内部的亚群或状态。
示例代码如下:
cluster_key = "leiden_res0_5" target_cluster = "3" adata_sub = adata[adata.obs[cluster_key] == target_cluster].copy() adata_sub 对子集重新构建邻居图并聚类:
sc.pp.neighbors( adata_sub, n_neighbors= 15 , n_pcs= 30 , random_state= 0 ) sc.tl.umap( adata_sub, min_dist= 0.3 , spread= 1.0 , random_state= 0 ) sc.tl.leiden( adata_sub, resolution= 0.5 , key_added= "leiden_sub" , flavor= "igraph" , n_iterations= 2 , random_state= 0 ) 查看子集聚类结果:
sc.pl.umap( adata_sub, color= "leiden_sub" , legend_loc= "on data" ) 亚群再聚类适合在已经有明确细胞大类之后使用。不要在还没有完成基础 QC、批次检查和全局聚类解释之前,过早进行过细的亚群拆分。
保存 保存完成聚类后的对象:
adata.write_h5ad( "/home/data/t090639/project/pbmc_10k/data/processed/06_pbmc_10k_clustered.h5ad" ) 检查文件是否保存成功:
os.path.exists( "/home/data/t090639/project/pbmc_10k/data/processed/06_pbmc_10k_clustered.h5ad" ) 保存后的对象中应至少包含:
adata.obs["leiden_res0_25"] :低分辨率 Leiden 聚类结果。 adata.obs["leiden_res0_5"] :中等分辨率 Leiden 聚类结果。 adata.obs["leiden_res0_8"] :较高分辨率 Leiden 聚类结果。
adata.obs["leiden_res1"] :高分辨率 Leiden 聚类结果。 adata.obsm["X_umap"] :UMAP 坐标。 adata.obsp["distances"] :邻居距离矩阵。 adata.obsp["connectivities"] :邻居连接矩阵。 adata.uns["neighbors"] :邻居图参数。 下一章可以基于这里保存的聚类结果继续进行 marker gene 筛选和细胞类型注释。