数据预处理:Seurat 流程与质量控制

说实话,我见过不少朋友一拿到 Visium 数据就急着跑聚类,结果出来的图根本没法解释。嗯,问题多半出在预处理上。预处理这一步没做好,后面选什么算法都是白搭。

我自己刚开始做空间转录组的时候,也踩过这个坑。当时的逻辑是:“反正后面有那么多聚类方法,总有一个能给我好结果吧?”结果花了三天调参数,还不如老老实实把 QC 做好。从那以后,我养成了一个习惯 —— 花 40% 的时间在预处理上。

为什么预处理这么重要?

空间转录组数据跟单细胞数据很像,但它多了一层位置信息。如果原始数据里混了太多死细胞点、线粒体比例过高的点,或者文库质量太差的点,后面的聚类算法会把这些异常点当成正常组织区域,导致边界模糊、误导生物结论。

说白了,预处理就是在“去伪存真”。只有把干净的数据交给下游算法,聚类结果才有复现性。

Seurat 标准流程概览

我习惯把 Seurat 的空间预处理分成六个核心步骤,下面这张图可以帮你快速建立整体印象:

Seurat 空间预处理流程 原始 Visium 数据 (raw counts + spots) QC 过滤 nFeature · nCount · %MT 归一化 LogNormalize (SCTransform) 高变基因检测 FindVariableFeatures ScaleData + RunPCA 降维,获取主成分 聚类前置准备 FindNeighbors + RunUMAP 我特别重视这一步

每一步都有它的门道。下面我把每个环节单独拆开讲,顺便说一说我踩过的坑。

关键步骤详解

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)。

💡 我的建议: 如果你拿到的是 raw_feature_bc_matrix(未过滤),一定要手动过滤掉那些没有组织覆盖的 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_Spatial200~5004000~6000太少可能是空点或死细胞;太多可能是 doublet 或组织边缘
nCount_Spatial500~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"))
⚠️ 我以前踩过的坑: 某次我直接把单细胞常用的 QC 阈值(比如 nFeature > 200)套到 Visium 数据上,结果扔掉了 40% 的 spot。后来才发现,Visium 的每个 spot 含有 1~10 个细胞,nFeature 天然比单细胞高。所以阈值应当放大 1.5-2 倍。

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 聚类,也能获得生物可解释的结果。