数据预处理:过滤、标准化、归一化
数据预处理这一步,我把它叫做“洗菜切菜”。说白了,原始数据拿到手,不能直接下锅炒。你得先把烂叶子摘掉,把泥洗干净,再切得大小均匀。Giotto 处理空间转录组数据也一样——过滤、标准化、归一化这三个动作,决定了你后面所有分析的质量。
我个人习惯,拿到表达矩阵和空间位置信息后,第一件事不是跑算法,而是先看一眼数据长什么样。你想想看,如果几千个基因在大部分 spot 里都是零,这种数据不做处理直接去跑聚类,结果很可能会跑偏。
第一步:质量控制与过滤
过滤的目标很明确——把质量差的细胞/spot 和表达不可靠的基因剔除掉。我曾经在一个项目中,拿着别人处理好的数据直接复现,结果聚类图谱一塌糊涂。后来回过头去查原始 QC,发现不少 spot 的 UMI 计数只有几十,明显是空 spot 或死细胞。白白浪费了两天时间。
Giotto 里常用的过滤参数,我列了个表,你可以对照着看:
| 过滤类型 | 常用参数 | 说明 | 我的建议阈值 |
|---|---|---|---|
| 基因过滤 | min_genes, max_genes | 剔除检测基因数过少或过多的 spot | min_genes=200, max_genes=5000 |
| UMI 过滤 | min_umis, max_umis | 剔除总 UMI 计数异常的 spot | min_umis=500, max_umis=20000 |
| 线粒体比例 | mito_prefix, mito_threshold | 剔除线粒体基因占比高的死细胞 | mito_prefix="MT-", mito_threshold=0.2 |
| 基因表达率 | min_cells | 剔除只在极少数 spot 中表达的基因 | min_cells=3 |
在 Giotto 里,过滤操作其实就几行代码:
# 创建 Giotto 对象
gobject <- createGiottoObject(
expression = raw_expr_matrix,
spatial_locs = spatial_locs_df
)
# 过滤 spot:剔除低质量细胞
gobject <- filterGiotto(
gobject,
min_genes = 200,
min_umis = 500,
max_umis = 20000,
mito_threshold = 0.2
)
# 过滤基因:剔除表达量太稀疏的基因
gobject <- filterGiotto(
gobject,
min_cells = 3
)
嗯,这里要注意filterGiotto默认是按顺序执行的。你先过滤了 spot,再过滤基因,这样保留下来的基因集合才可靠。反过来先过滤基因再过滤 spot,逻辑上也没问题,但计算效率会差一些。
第二步:标准化——消除技术偏差
标准化解决的是“不同 spot 之间测序深度不一致”的问题。说白了,一个 spot 测到 1000 个 UMI,另一个 spot 测到 10000 个 UMI,不代表后者基因表达量真的高了 10 倍。可能只是那个 spot 捕获效率更高。标准化就是把这些技术差异抹平。
Giotto 支持多种标准化方法。我做过对比测试,不同方法对下游结果影响不小,尤其是在鉴定稀有细胞类型时。你选的方法不对,可能直接就把那 1% 的细胞群给淹没了。
| 方法 | 原理 | 适用场景 | Giotto 参数名 |
|---|---|---|---|
| library_size | 除以总 UMI,乘以尺度因子 | 通用场景,计算快 | method = "library_size" |
| log2 | library_size 后取 log2 | 适合下游使用正态分布模型 | method = "log2" |
| pearson | 基于 Pearson 残差标准化 | 高 dropout 数据,保留低表达基因 | method = "pearson" |
| rank | 将表达量转为秩次 | 非参数检验、鲁棒性要求高时 | method = "rank" |
log2 就够。如果数据比较“脏”——比如有 batch effect 或者 dropout 率很高——我会优先试 pearson。它能在保留真实生物信号的同时,更好地抑制技术噪声。
标准化代码也很直接:
# 方法一:library_size + log2
gobject <- normalizeGiotto(
gobject,
method = "log2",
scale_factor = 10000
)
# 方法二:Pearson 残差标准化
gobject <- normalizeGiotto(
gobject,
method = "pearson"
)
我记得早期做 scRNA-seq 时,大家几乎清一色用 log2。后来空间转录组兴起,dropout 率比单细胞还高,pearson 标准化才逐渐被大家接受。你想想看,一个 spot 里可能只有 50% 的基因被检测到,剩下的全是零。这种情况用 log2 做标准化,那些未被检测到的基因并不是“真的不表达”——只是没被捕获到而已。
第三步:归一化——让数据落在同一量纲
标准化之后,数据量纲还是基因级别的(每个基因有自己的表达范围)。归一化是把所有基因拉到同一个尺度上,比如均值为0、方差为1。这不影响基因之间的相对关系,但能让你在用 PCA、UMAP 这些降维方法时,不被高表达基因“带偏”。
Giotto 的归一化默认就是 Z-score(标准分数)。你基本不需要自己调参数,不过我还是习惯看一眼归一化后的分布直方图,确保没有异常值。
# 归一化(默认 Z-score)
gobject <- adjustGiotto(
gobject,
method = "zscore"
)
# 你也可以手动控制哪些维度做归一化
gobject <- adjustGiotto(
gobject,
method = "zscore",
vars_to_regress = c("total_umis", "percent_mito")
)
归一化的另一个作用,是帮你把不想看到的技术变量“回归掉”。比如总 UMI 数和线粒体比例,它们虽然已经在过滤时被控制,但残留的影响仍然存在。通过 vars_to_regress 参数,你可以把它们作为一个协变量移除。我经常在项目里做这一步,尤其是当组织样本里有坏死区域时——坏死区域的线粒体比例会偏高,不回归掉的话,后续聚类可能会把坏死和正常组织区分成两个独立的细胞群。
现在,我们用一张图来梳理整个预处理流程:
预处理做完,你的 Giotto 对象里会新增两个矩阵:一个叫normalized_expr(标准化后的表达量),一个叫scaled_expr(归一化后的表达量)。这两个矩阵是后续降维、聚类、可视化操作的基础。
最后再啰嗦一句:我建议你把过滤前后的数据统计量打印出来,保存成日志。这一步不花时间,但在你回头检查结果或者写论文时,能省去很多重复劳动。
# 查看预处理前后的数据量
print(dim(raw_expr_matrix))
print(dim(gobject@expression$raw$expr))
# 打印过滤统计
print(gobject@filter_stats)