数据预处理:Seurat 流程与质量控制
说实话,我见过不少朋友一拿到 Visium 数据就急着跑聚类,结果出来的图根本没法解释。嗯,问题多半出在预处理上。预处理这一步没做好,后面选什么算法都是白搭。
我自己刚开始做空间转录组的时候,也踩过这个坑。当时的逻辑是:“反正后面有那么多聚类方法,总有一个能给我好结果吧?”结果花了三天调参数,还不如老老实实把 QC 做好。从那以后,我养成了一个习惯 —— 花 40% 的时间在预处理上。
为什么预处理这么重要?
空间转录组数据跟单细胞数据很像,但它多了一层位置信息。如果原始数据里混了太多死细胞点、线粒体比例过高的点,或者文库质量太差的点,后面的聚类算法会把这些异常点当成正常组织区域,导致边界模糊、误导生物结论。
说白了,预处理就是在“去伪存真”。只有把干净的数据交给下游算法,聚类结果才有复现性。
Seurat 标准流程概览
我习惯把 Seurat 的空间预处理分成六个核心步骤,下面这张图可以帮你快速建立整体印象:
每一步都有它的门道。下面我把每个环节单独拆开讲,顺便说一说我踩过的坑。
关键步骤详解
1. 数据读取与过滤
Visium 数据通常包含三个文件:原始计数矩阵(filtered_feature_bc_matrix.h5)、组织图像(tissue_lowres_image.png)、以及 spot 坐标(tissue_positions_list.csv)。Seurat 的 Load10X_Spatial() 可以一次性读入。
# 读入 Visium 数据
library(Seurat)
library(SeuratData)
library(ggplot2)
data_dir <- "path/to/spaceranger/outs/"
sobj <- Load10X_Spatial(data.dir = data_dir,
filename = "filtered_feature_bc_matrix.h5",
assay = "Spatial",
slice = "slice1",
filter.matrix = TRUE)
filter.matrix = TRUE 是默认值,会去掉空点(没有组织覆盖的 spot)。
2. 质量控制指标
我常用的 QC 指标有三个:nFeature_Spatial(每个 spot 检测到的基因数)、nCount_Spatial(总 UMI 数)、percent.mt(线粒体基因比例)。
# 计算线粒体比例
sobj[["percent.mt"]] <- PercentageFeatureSet(sobj, pattern = "^MT-")
# 查看 QC 分布
VlnPlot(sobj, features = c("nFeature_Spatial", "nCount_Spatial", "percent.mt"),
ncol = 3, pt.size = 0.01) + NoLegend()
那阈值怎么定?我个人的经验是这样的:
| 指标 | 典型低值阈值 | 典型高值阈值 | 理由 |
|---|---|---|---|
| nFeature_Spatial | 200~500 | 4000~6000 | 太少可能是空点或死细胞;太多可能是 doublet 或组织边缘 |
| nCount_Spatial | 500~1000 | 依组织类型浮动 | UMI 太少说明 RNA 捕获效率低 |
| percent.mt | — | 10%~20% | 太高说明细胞正在死亡或线粒体污染 |
但别死搬硬套。我在做小鼠大脑切片时,髓鞘丰富的区域 nFeature 天生偏低,这时候如果把阈值设成 500 就会扔掉很多白质区的点。所以 先画一张 SpatialFeaturePlot 看看 bad spots 的位置,再做决定。
SpatialFeaturePlot(sobj, features = "percent.mt") +
scale_fill_gradientn(colors = c("gray90", "orange", "red"))
3. 过滤与归一化
# 过滤
sobj <- subset(sobj,
subset = nFeature_Spatial > 500 &
nFeature_Spatial < 5000 &
percent.mt < 20)
# 标准归一化
sobj <- NormalizeData(sobj, normalization.method = "LogNormalize", scale.factor = 10000)
# 高变基因
sobj <- FindVariableFeatures(sobj, selection.method = "vst", nfeatures = 2000)
归一化方法我通常先用 LogNormalize。如果组织内有明显的批次效应或技术噪音,可以考虑 SCTransform,它用正则化负二项回归一次性完成归一化、方差稳定化、以及高变基因选择。但 SCTransform 计算量较大,我用过几次后觉得它对大样本更友好。
4. ScaleData 与 PCA
在高变基因上做 scale 和 PCA,这步没什么技术难点,但注意 scale.factor 的设定:
# 只对高变基因做缩放(加速)
sobj <- ScaleData(sobj, features = VariableFeatures(sobj))
# PCA
sobj <- RunPCA(sobj, features = VariableFeatures(sobj), npcs = 30)
PCA 之后一定要用 ElbowPlot 确定合适的主成分个数,一般取拐点前 10~20 个 PC 即可。不需要贪多,加太多噪音成分反而会干扰后续聚类。
完整的预处理封装函数
下面是我自己项目中常用的一个包裹函数,可以直接用在 Visium 对象上:
#' Spatial 数据预处理封装
#' @param sobj Seurat 对象
#' @param min_nFeature 默认 500
#' @param max_nFeature 默认 5000
#' @param max_pMT 默认 20
#' @param nfeatures_var 高变基因数 2000
#' @param npcs PCA 主成分数 30
preprocess_visium <- function(sobj,
min_nFeature = 500,
max_nFeature = 5000,
max_pMT = 20,
nfeatures_var = 2000,
npcs = 30) {
# 计算 QC 指标
sobj[["percent.mt"]] <- PercentageFeatureSet(sobj, pattern = "^MT-")
# 过滤
sobj <- subset(sobj,
subset = nFeature_Spatial > min_nFeature &
nFeature_Spatial < max_nFeature &
percent.mt < max_pMT)
# 归一化
sobj <- NormalizeData(sobj, verbose = FALSE)
# 高变基因
sobj <- FindVariableFeatures(sobj, selection.method = "vst",
nfeatures = nfeatures_var, verbose = FALSE)
# Scale + PCA
sobj <- ScaleData(sobj, features = VariableFeatures(sobj), verbose = FALSE)
sobj <- RunPCA(sobj, features = VariableFeatures(sobj), npcs = npcs, verbose = FALSE)
return(sobj)
}
# 一行调用
sobj <- preprocess_visium(sobj)
常见坑与避坑指南
- 坑 1:过滤后剩下少于 100 个 spot。 大概率是阈值设太严。我建议先画
SpatialFeaturePlot看看坏点的空间分布,视觉上确认后再调参。 - 坑 2:高变基因选了默认的 2000,结果 PCA 图中的分离度很差。 可以考虑增加到 3000~5000,尤其在组织异质性较高的样本(如肿瘤)。
- 坑 3:用 SCTransform 时内存爆了。 如果组织切片包含几万个 spot,建议先做 fastMNN 或者选批次平衡策略。我个人的变通做法是:先 LogNormalize 跑一遍快速调参,最后定稿时再换成 SCTransform。
- 坑 4:比例缩放后基因表达值全为 0。 检查一下之前是否过滤掉了所有高表达基因,或者 scaling 时用了全基因集而非仅高变基因。我习惯显式指定
features = VariableFeatures(sobj)来避免这个问题。
质量控制才是聚类的基石。 我见过太多人在这一步贪快,直接用默认参数跑完。结果后面无论怎么调聚类算法,分出来的群都跟病理切片对不上。反过来,如果你能耐心把 QC 做到位 —— 看分布图、写自定义阈值函数、手工验证过滤逻辑 —— 哪怕你用最简单的 Louvain 聚类,也能获得生物可解释的结果。