3、Seurat V5 数据结构变化:SeuratObject v5 与 Layers 架构
各位,如果之前用过 Seurat V4,那打开 V5 后第一感觉可能是:诶,怎么 GetAssayData 不灵了?别急,我来慢慢讲。
V5 最核心的变化,就是把原来每个 Assay 里的几个固定 slot(counts、data、scale.data)给拆掉了,换成了 Layers 架构。说白了,就是让数据矩阵的存放方式更灵活,也支持多模态数据更自然的融合。我在给一个多组学项目迁移代码时,差点被这个新结构绊倒,但适应之后发现确实香。
3.1 从 Assay 到 Assay5
V4 时代,每个 Assay 对象有 counts、data、scale.data 三个固定的 slot。如果你要存个 log2 变换的矩阵?只能自己塞进 @misc 里。V5 的 Assay5 类直接扔掉 slot 概念,改用一个 layers 列表来存放所有矩阵。
这样做的好处很明显:
- 你可以定义任意名字的 layer,比如
"logcounts"、"norm_dge"。 - 各 layer 可以是不同的数据类型(稀疏矩阵、稠密矩阵、HDF5 引用等)。
- 未来容易扩展新模态。
对象结构对比看下面这张表,你就懂了。
| 维度 | Seurat V4 (Assay) | Seurat V5 (Assay5) |
|---|---|---|
| 数据容器 | 三个固定 slot: counts, data, scale.data | 可变 Layers 列表 |
| 矩阵名 | 由对象属性确定 | 用户自定义,例如 "counts", "log1p" |
| 访问方式 | GetAssayData(..., slot="data") | [["layer_name"]] 或 $layer_name |
| 添加新数据 | 需写入 @misc 或创建新 Assay | 直接 seu[["RNA"]]$new_layer <- mat |
| 返回对象 | Assay | Assay5 |
嗯,V5 的这个设计,其实更像一个「字典式」的存储结构,你看下面这张图就更清楚了。
3.2 Layers 架构的核心设计
你可能要问:怎么切换不同 layer 的数据来做下游分析?其实 Seurat V5 内部会默认查找名为 "data" 的 layer 作为「默认层」,如果找不到,就用 counts。你在跑 NormalizeData、ScaleData 时,函数会自动创建一个新 layer,比如 NormalizeData 会生成 "log1p" 层(如果使用的是默认的 LogNormalize)。
我记得第一次用 V5 时,发现 GetAssayData 不能用了,还以为是 bug。后来才明白,官方推荐用 [[ 操作符:
# V4 写法
GetAssayData(pbmc, assay = "RNA", slot = "data")
# V5 写法
pbmc[["RNA"]]$data # 返回 "data" layer 的矩阵
pbmc[["RNA"]][["log1p"]] # 返回指定 layer 的矩阵
当然,还保留了一个兼容函数 LayerData,但我觉得直接使用 $ 更直观。
3.3 对象访问方式的改变
V5 的 SeuratObject 本身也做了调整。原来通过 @assays 槽位访问 assay 列表的方式,现在建议改用 [[ 或者 $。看看下面几个常用操作的变化:
| 操作 | V4 | V5 |
|---|---|---|
| 获取所有 assay 名 | names(pbmc@assays) | names(pbmc) |
| 获取某个 assay | pbmc[["RNA"]] 或 pbmc@assays$RNA | pbmc[["RNA"]] |
| 获取矩阵数据 | GetAssayData(pbmc, slot="data") | pbmc[["RNA"]]$data |
| 添加矩阵 | pbmc[["RNA"]]@counts 或 SetAssayData | pbmc[["RNA"]]$new_layer <- mat |
| 删除 layer | 不支持直接删除 | pbmc[["RNA"]]$old_layer <- NULL |
GetAssayData 替换成 pbmc[["RNA"]]$data 的形式。如果你有多个 assay,记得把 assay 名字参数也带上。
为什么 V5 要这么改?你想想看,原来 slot 是固定的,想加个新变换矩阵就得 @misc 里塞,既不规范也不好管理。现在全用 Layers,每个 layer 有自己的名字,想删就删,想改就改,数据流更清晰。
3.4 实战迁移示例
假设你有一段 V4 的标准分析流程,我们来逐行迁移:
# ===== V4 代码 =====
pbmc <- NormalizeData(pbmc, normalization.method = "LogNormalize", scale.factor = 10000)
pbmc <- FindVariableFeatures(pbmc, selection.method = "vst", nfeatures = 2000)
pbmc <- ScaleData(pbmc, vars.to.regress = "percent.mt")
# 获取标准化后的矩阵
norm_mat <- GetAssayData(pbmc, slot = "data")
var_genes <- VariableFeatures(pbmc)
# ===== V5 迁移后 =====
pbmc <- NormalizeData(pbmc) # 默认创建 "log1p" layer
pbmc <- FindVariableFeatures(pbmc) # 自动使用 counts layer
pbmc <- ScaleData(pbmc, vars.to.regress = "percent.mt") # 创建 "scale.data" layer
# 获取标准化后的矩阵(注意这里!)
norm_mat <- pbmc[["RNA"]]$log1p
# 或者用 LayerData
norm_mat <- LayerData(pbmc, assay = "RNA", layer = "log1p")
var_genes <- VariableFeatures(pbmc)
你看,除了矩阵获取那一步,其他基本没变。NormalizeData 默认产生 "log1p" 层,而不是覆盖 "data" 层。我刚开始时以为它还会像 V4 一样叫 "data",结果一直拿不到数据,排查了半天…… 嗯,所以这里要格外注意。
3.5 避坑与建议
merge 合并多个 Seurat 对象时,如果各个对象的 layer 名字不一致(比如一个叫 "log1p",另一个叫 "norm"),合并后会自动创建多个分组的 layer(如 "log1p.X"、"norm.Y"),导致后续分析找不到数据。建议统一 layer 命名规范,或者合并前只保留 "counts" 层再合并。
另外,如果你习惯用 split.by 做整合分析(如 SCTransform 或 IntegrateLayers),V5 会自动把不同样本的矩阵存成以样本名为后缀的子层,例如 "counts.ctrl"、"counts.stim"。这在管理大项目时非常有用,但注意自定义函数时可能需要遍历这些子层。
- 安装 V5 后,先跑一遍官方迁移脚本
options(Seurat.object.assay.version = "v5")。 - 用
library(Seurat); update(seu)升级旧对象。 - 检查
names(seu[["RNA"]])看有哪些 layer。 - 将脚本中所有
GetAssayData、SetAssayData、@assays替换为 V5 风格。
最后送你个速查表,帮你快速回忆 V5 的 layer 默认命名规则:
| 函数 | 默认创建的 layer 名 |
|---|---|
NormalizeData(..., normalization.method="LogNormalize") | "log1p" |
NormalizeData(..., normalization.method="CLR") | "data" |
ScaleData | "scale.data" |
FindVariableFeatures | 不创建新 layer,仅存储基因列表 |
RunPCA | 不创建 layer,结果存入 reductions |
好了,Seurat V5 的数据结构变化,核心就是「拆 slot,立 layers」。适应之后,你会发现这种设计对多模态数据、大矩阵管理都更友好。别怕改代码,改一次就习惯了。