第三章:数据预处理——标准化、降维、聚类
预处理这一步,说白了就是给原始数据“洗个澡”,让它能反映真实的生物学信号。空间转录组的数据尤其麻烦——它既有基因表达的稀疏性,又有空间位置带来的批次效应。
我个人习惯把这三件事串起来做:先标准化抹平测序深度的差异,再用降维压缩信息,最后用聚类把细胞或spot分群。顺序千万别搞反,否则你后续分析全是坑。
3.1 标准化——摆脱文库大小的影响
不同spot捕获到的总RNA量不一样,这是死理。直接拿原始UMI counts做差异分析,结果会严重偏向高表达基因。我见过有人跳过标准化直接跑聚类,结果分出来的群全是按测序深度分的,毫无生物学意义。
| 方法 | 原理 | 适用场景 | 缺点 |
|---|---|---|---|
| LogNormalize | 每个spot的基因 counts 除以该spot总counts,乘以scale因子,再log1p | 一般空间数据,快速简便 | 不能完全消除技术噪声 |
| SCTransform | 用正则化负二项回归模型,直接输出Pearson残差 | 数据异质性大、有dropout | 计算慢,占内存大 |
| Robust normalization | 基于分位数回归或spikes调整 | 跨平台整合 | 参数敏感 |
我自己在10x Visium数据上最常用LogNormalize:
library(Seurat)
seurat_obj <- NormalizeData(seurat_obj,
normalization.method = "LogNormalize",
scale.factor = 10000)
但如果你用的是MERFISH或者Slide-seq这类高分辨率技术,SCTransform更合适。它能把技术方差和生物学方差解耦开。
我曾经在一个MERFISH项目里用LogNormalize跑聚类,结果spot边界特别模糊。换成SCTransform之后,细胞类型的边界一下就清晰了——嗯,这里要注意,SCTransform对内存要求高,建议先过滤掉低表达基因再跑。
FindVariableFeatures,只保留高变基因(前2000~3000个)。这样既能降噪,又能让后续降维更稳定。
3.2 降维——把高维表达降成低维坐标
标准化后的数据通常还有几千个基因(或特征),直接聚类很难。降维的目的就是找出“最重要的几个维度”,让空间点云更容易被分割。
主成分分析(PCA)是绕不开的基础操作。它把基因表达线性投影到方差最大的方向上。我建议至少保留30~50个PC,否则会丢失稀有细胞类型的信息。
seurat_obj <- RunPCA(seurat_obj,
npcs = 50,
features = VariableFeatures(seurat_obj))
为什么是50?你想想,空间转录组每个spot可能混合了两三种细胞,只取20个PC根本抓不住细微差异。我在一个肿瘤切片里遇到过,PC30才捕捉到一种浸润免疫细胞的信号——要是只留20个PC,那群细胞就彻底消失了。
除了PCA,还有几个常用降维方法:
- UMAP/t-SNE:主要用于可视化,不直接用于聚类。
- Harmony:专门去除批次效应的嵌入方法,常接在PCA之后。
- scVI(深度学习):能同时处理批次效应和零膨胀,但计算量大。
我的一般流程:先跑PCA + Harmony,然后用Harmony的嵌入做聚类。
避坑指南
- 别拿原始counts直接跑PCA——方差会完全被高表达基因主导,你得到的PC全是管家基因的信号。
- PCA前最好把数据做
ScaleData(以基因中心化并除以标准差),否则高表达基因权重过大。 - Harmony本质上是在PCA空间里做迭代校正,对分群离散的样本很有效;但如果你的样本有很强的批次结构(比如同一张切片的不同区域),Harmony可能过度矫正,把真实生物差异也抹掉。
# 在Seurat中使用Harmony
library(harmony)
seurat_obj <- RunHarmony(seurat_obj,
group.by.vars = "orig.ident",
reduction = "pca",
dims = 1:30)
3.3 聚类——找到空间上的“邻居团”
降维之后,我们终于可以回答核心问题:哪些spot的转录组特征最相似?
空间转录组的聚类和单细胞不一样——spot之间还有空间坐标。你可以用纯转录组的相似性(基于降维距离),也可以结合空间邻域关系。两种思路我都做过:
| 方法 | 输入 | 优点 | 风险 |
|---|---|---|---|
| Louvain / Leiden(纯转录组) | 降维后的距离矩阵 | 成熟稳定,社区支持好 | 忽略空间距离,可能把分散的相同细胞类型连成一个cluster |
| BayesSpace(空间邻域) | PCA + 空间坐标 | 明确利用邻域结构,边界更清晰 | 参数多,调参麻烦 |
| Giotto(空间感知聚类) | 空间网络 + 表达矩阵 | 灵活,自定义邻域定义 | 学习成本略高 |
我个人的默认做法是先用Leiden做转录组聚类,然后用spatial distance做后处理合并。原因很简单——纯Leiden跑得快,结果稳定。如果后续发现某些集群在空间上破碎或者跨区混合,再换成BayesSpace精细校正。
# Seurat中运行Leiden聚类
seurat_obj <- FindNeighbors(seurat_obj,
reduction = "harmony",
dims = 1:30)
seurat_obj <- FindClusters(seurat_obj,
resolution = 0.6, # 分辨率控制粒度
algorithm = 4) # 4 = Leiden
分辨率这个参数特别关键。我习惯先用0.4和0.8各跑一次,然后结合空间特征看一下:高分辨率下如果出现大量小于5个spot的“微簇”,就说明过碎了。反之分辨率太低会把明显不同的细胞类型压在一起。
3.4 整个预处理流程的“流水线”
把上面三个步骤串起来,就是一张清晰的流程图:
你应该能看出,我习惯把可视化和分辨率调优放在并行分支里,而不是线性流程的一部分。因为在得到初步聚类结果后,我总要回头看看UMAP和空间分布图,再决定要不要调整参数——这不是一次性的,往往要迭代两三轮。
3.5 常见坑与我的处理方式
- 标准化后出现负值? 别慌,SCTransform输出Pearson残差,负值表示比期望值低。这在生物学上没问题,但你可以手动加一个偏移量(比如最小值+1)让所有值非负,不过我很少这么做,因为降维和聚类算法对负值并不敏感。
- 降维时发现PC方差已经饱和? 比如第20个PC之后方差没有明显下降,这通常意味着数据中有冗余的“技术噪声”,我一般会选择取前30个PC,而不是所有方差贡献显著的PC。
- 聚类结果中有孤立的小群? 如果那个小群只有1~3个spot且远离其他主群,大概率是dropout噪声。我会手动过滤掉它们(小于5个spot的cluster视为噪声),或者在
FindClusters中设min.size = 5。
最后一点经验分享:
我刚做空间转录组时,喜欢把标准化、降维、聚类一次性写好脚本跑完,然后直接奔着差异基因去。结果发现很多cluster在空间上根本解释不了——要么是碎片化的,要么是噪声群。后来我养成了一个习惯:每做完一步,先可视化再往下一步。标准化后看log-expression分布,降维后看PC loading和UMAP,聚类后看空间分布。哪怕多花10分钟,也比跑完整个流程再返工强。