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
返回对象AssayAssay5

嗯,V5 的这个设计,其实更像一个「字典式」的存储结构,你看下面这张图就更清楚了。

Seurat V5 对象结构:Assay5 与 Layers Seurat V5 对象内部结构(简化) Seurat 对象 包含多个 Assay5,例如 RNA、ADT、HTO Assay5 (RNA) 元数据:features, cells, meta.features… Layers 列表(可变) "counts" "log1p" "scale.data" 提示:你还可以添加任意自定义 layer,比如 "vst_residuals"、 "pearson_residuals",直接 seu[["RNA"]]$custom_mat <- your_matrix

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 列表的方式,现在建议改用 [[ 或者 $。看看下面几个常用操作的变化:

操作V4V5
获取所有 assay 名names(pbmc@assays)names(pbmc)
获取某个 assaypbmc[["RNA"]] 或 pbmc@assays$RNApbmc[["RNA"]]
获取矩阵数据GetAssayData(pbmc, slot="data")pbmc[["RNA"]]$data
添加矩阵pbmc[["RNA"]]@counts 或 SetAssayDatapbmc[["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 避坑与建议

我曾经踩过的坑:在 V5 中使用 merge 合并多个 Seurat 对象时,如果各个对象的 layer 名字不一致(比如一个叫 "log1p",另一个叫 "norm"),合并后会自动创建多个分组的 layer(如 "log1p.X"、"norm.Y"),导致后续分析找不到数据。建议统一 layer 命名规范,或者合并前只保留 "counts" 层再合并。

另外,如果你习惯用 split.by 做整合分析(如 SCTransform 或 IntegrateLayers),V5 会自动把不同样本的矩阵存成以样本名为后缀的子层,例如 "counts.ctrl"、"counts.stim"。这在管理大项目时非常有用,但注意自定义函数时可能需要遍历这些子层。

我的推荐做法:
  1. 安装 V5 后,先跑一遍官方迁移脚本 options(Seurat.object.assay.version = "v5")。
  2. 用 library(Seurat); update(seu) 升级旧对象。
  3. 检查 names(seu[["RNA"]]) 看有哪些 layer。
  4. 将脚本中所有 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」。适应之后,你会发现这种设计对多模态数据、大矩阵管理都更友好。别怕改代码,改一次就习惯了。