数据预处理(QC):质量控制指标、过滤与组织提取

各位,正式上机分析之前,最磨人的一步来了——数据预处理。说白了就是给你的空间转录组数据“体检”。我做了这么多年项目,可以很负责任地告诉你:这一步省掉的时间,最后都会变成修 bug 的时间。咱们直接开整。

质量控制指标:先了解你的数据“体质”

不管是用 10x Visium、Slide-seq 还是 MERFISH,拿到手的 Raw Data 都会包含几个核心指标。我个人习惯用一个矩阵一张表来快速扫一眼所有 spots 的分布。

指标含义典型异常我见过的极端案例
UMI 计数 (nCount_RNA)每个 spot 捕获到的转录本总数过低(< 500)或过高(> 10000)某样本有 30% spots UMI 数 < 100,后来发现是组织切片贴歪了
检测基因数 (nFeature_RNA)每个 spot 检测到的不同基因种类过低(< 200)或过高(> 8000)
线粒体基因比例 (percent.mt)线粒体基因 UMI 占总 UMI 的比例过高(> 20%)说明细胞破损或死细胞多海马体样本线粒体比例普遍在 35% 以上,并非异常,而是神经元能量需求高
核糖体基因比例 (percent.ribo)核糖体基因占比过高可能污染
我的小技巧: 别只看平均值。把这三个指标画成小提琴图 + 散点图(比如 nCount vs percent.mt),一眼就能揪出异常斑点。Seurat 自带的 VlnPlot 和 FeatureScatter 够用了。

过滤低质量 spots:该删就删,别手软

拿到上面的指标分布后,阈值怎么定?我早期吃过亏,直接套用 scRNA-seq 的阈值,结果把组织边缘的稀疏区域全干掉了——后面批次校正时模型学不到局部结构。血的教训。

正确的打开方式:先看数据分布,再定阈值。

# R 语言示例(Seurat)
library(Seurat)
spatial_obj <- CreateSeuratObject(counts = raw_counts, assay = "Spatial")
spatial_obj[["percent.mt"]] <- PercentageFeatureSet(spatial_obj, pattern = "^MT-")

# 我习惯用分位数来定阈值,不拍脑袋
mt_low <- quantile(spatial_obj$percent.mt, 0.02)  # 2% 分位
mt_high <- quantile(spatial_obj$percent.mt, 0.98) # 98% 分位
nCount_low <- quantile(spatial_obj$nCount_Spatial, 0.01)

spatial_obj <- subset(spatial_obj,
  subset = nCount_Spatial > nCount_low &
           percent.mt > mt_low & percent.mt < mt_high)

注意!线粒体比例 过低 也要警惕。我碰到过一次:某个 spot 的 perecent.mt = 0%,查了半天发现是那个位置的 barcode 根本没捕获到真细胞,就是背景噪声。所以最好设一个下限(比如 > 1%),把一杆空枪都过滤掉。

避坑指南: 过滤后务必在组织切片图像上可视化剩余 spots 的位置。如果丢掉的是某块组织区域(比如肿瘤边缘),很可能是切片过程中该区域细胞活力不够,而非真正低质量。这时候要重新评估阈值,或者保留该区域单独分析。

图像配准与组织区域提取:让 spot 和像素对上号

空间转录组的特点就是位置信息。10x Visium 的输出里有个 spatial/tissue_lowres_image.png 和 tissue_positions_list.csv。但配准不一定完美,尤其是当组织切片有褶皱、气泡或者染色不均的时候。

我的标准流程分三步:

  1. 读取原始图像与坐标文件。手动检查组织区域是否被正确识别(通常用 H&E 或 IF 染色背景来判定组织边界)。
  2. 手动或自动对齐。Seurat 提供了 Read10X_Image 函数,但只适用于标准配准。当用户自定义实验(比如多张切片拼图)时,我建议用 EBImage 或 OpenCV 做仿射变换,把 spots 坐标映射到图像像素上。
  3. 提取组织区域 mask。可以用图像分割(如 Otsu 阈值法、分水岭)自动去掉背景区域,只保留组织内部 spots。这个步骤能显著减少下游计算量,也避免了背景 spots 对批次校正的干扰。
# Python 示例(OpenCV 做简单组织区域提取)
import cv2
import numpy as np

img = cv2.imread("tissue_lowres_image.png", cv2.IMREAD_GRAYSCALE)
_, mask = cv2.threshold(img, 0, 255, cv2.THRESH_BINARY + cv2.THRESH_OTSU)
# 再做形态学闭合操作,填掉小空洞
kernel = cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (15,15))
mask = cv2.morphologyEx(mask, cv2.MORPH_CLOSE, kernel)

# 然后根据 mask 过滤 spots
核心逻辑: 图像配准不是为了“好看”,而是为了后续做批次校正时获得更精确的邻域矩阵。如果你在配准环节就丢了 20% 的 spots,那校正的 warping 函数会学到错误的局部变形。

嗯,这里要注意,配准的精度决定了后面所有空间分析的准确性。我曾在项目中遇到两个批次的组织切片旋转角度差了 30°,直接用默认配准函数跑出来一团糟。后来手动加上旋转矩阵和像素重采样才搞定。

下面我画了一张图,把本章的 QC 流程串起来。你跟着它走,基本不会漏掉关键节点。

空间转录组数据预处理 QC 流程图 原始 Count 矩阵 + 图像 计算 UMI 数、基因数、线粒体比例 基于分位数/阈值过滤低质量 spots 图像配准:坐标映射 + 仿射变换 组织区域提取(Otsu / 分水岭) 关键输出 • 过滤后的 spots 集合 • 对齐的组织图像 常见坑 • 旋转角度不对 • 组织碎片误判为背景

这张图你应该能看懂了:从原始矩阵和图像出发,先算三个核心指标,再基于分布定阈值过滤,之后做图像配准让 spots 位置与像素对齐,最后提取组织区域、去掉背景。这四步做完,你的数据才算是“洗白白”,可以放心喂给下游的批次校正模型了。

实战提醒: 我强烈建议把过滤前后的 spot 分布画在同一张空间散点图上。T 检验和 Z-score 都是骗人的,眼睛和先验知识才是最好的 QC 工具。如果一个 spot 的基因表达几乎全是线粒体基因,果断删掉,不要犹豫。