第4章:创建SingleCellExperiment对象——核心数据结构讲解

在单细胞数据分析中,数据怎么存,直接影响后面一切操作的效率和正确性。我见过不少新手,一开始就把表达矩阵扔进一个普通 data.frame 里,做着做着就发现行名丢了、列名乱了、元数据对不上了。

所以这一章,咱们聚焦在 SingleCellExperiment 这个核心容器上。说白了,它就是为单细胞数据量身定做的 DataFrame Plus。

4.1 为什么非要用 SCE?

你想想看,一个单细胞实验里,我们至少需要存这些东西:

  • 表达矩阵(基因×细胞)
  • 细胞元数据(样本来源、批次、处理条件等)
  • 基因元数据(基因类型、是否高变基因等)
  • 降维结果(PCA、t-SNE、UMAP 坐标)
  • 质控统计量(库大小、检测基因数、线粒体比例等)

如果用多个独立变量去存,很容易出现索引不一致。我就掉过这个坑——有一次把表达矩阵和细胞注释的行顺序搞反了,结果跑了半天的聚类,发现全是错的。

SCE 解决了这个痛点。它把所有数据整齐地放进一个对象里,按“基因 x 细胞”二维结构统一索引,再也不怕对应不上。

对比项 普通 data.frame / 独立变量 SingleCellExperiment
数据组织 分散在各处 统一容器
索引对齐 需手动维护 内置行/列名绑定
扩展性 差 支持任意附属数据
内存效率 一般 支持稀疏矩阵、延迟计算

一句话总结:只要接触到 scran 的质控、标准化、降维等后续步骤,你基本绕不开 SCE。

4.2 SCE 对象的核心 Slots

一个 SCE 对象的结构,可以想象成一栋四层小楼,每层负责不同的数据类型。

library(SingleCellExperiment)

# 模拟一个最简单的SCE对象
counts_matrix <- matrix(rpois(100, lambda = 10), nrow = 20, ncol = 5)
rownames(counts_matrix) <- paste0("gene", 1:20)
colnames(counts_matrix) <- paste0("cell", 1:5)

sce <- SingleCellExperiment(
    assays = list(counts = counts_matrix)
)

sce

4.3 四个主层:assays, colData, rowData, metadata

4.3.1 assays —— 表达数据的核心层

这是主角。assays 可以放多个表达矩阵,比如 raw counts、归一化后的 logcounts、TPM 等。每个矩阵的维度必须一样(基因×细胞)。

# 添加第二个assay
logcounts(sce) <- log2(counts(sce) + 1)

# 看看现在有几个assays
assayNames(sce)

为什么需要多个?我在项目中发现,早期拿 counts 做质控最靠谱,但后续降维和可视化时 log-normalized 数据更合适。都保留在同一个对象里,切换起来很方便。

我个人习惯:始终保留 raw counts 作为第一个 assay,免得后续标准化方法不满意,还得重新从源头算。

4.3.2 colData —— 细胞级别的元数据

colData 是一个 DataFrame,每一行对应一个细胞,每一列对应一个属性。比如:样本 ID、批次、质控通过/未通过、UMAP 聚类标签等。

# 添加细胞元数据
colData(sce)$sample_id <- rep(c("sample1", "sample2"), length.out = ncol(sce))
colData(sce)$pass_qc <- c(TRUE, TRUE, FALSE, TRUE, FALSE)

# 还可以一步到位创建
sce$total_counts <- colSums(counts(sce))
sce$detected_genes <- colSums(counts(sce) > 0)

这里有个小技巧:直接用 sce$列名 就能存取,和操作 data.frame 没什么两样。

4.3.3 rowData —— 基因级别的元数据

用来记录每个基因的信息:是否线粒体基因、是否核糖体基因、基因符号、是否被鉴定为高变基因等。

# 标记线粒体基因
rowData(sce)$is_mito <- grepl("^MT-", rownames(sce))

# 标记核糖体基因
rowData(sce)$is_ribo <- grepl("^RPL|^RPS", rownames(sce))

嗯,这里要注意:在人类基因中,线粒体基因以 "MT-" 开头;小鼠则是 "mt-"。不同物种别搞混了。

4.3.4 metadata —— 实验级别的元数据

放一些不针对某个细胞或基因的整体信息,比如:实验日期、使用的软件版本、质控阈值、分析流水线参数等。

metadata(sce)$experiment_date <- "2024-03-15"
metadata(sce)$scran_version <- packageVersion("scran")
metadata(sce)$qc_threshold <- list(
    min_library_size = 500,
    max_mito_ratio = 0.2
)

这东西看起来不起眼,但我在复现别人结果时,经常靠 metadata 来确认参数配置,省了很多翻脚本的时间。

4.4 两个常用附属组件

4.4.1 reducedDims —— 降维结果存放处

做 PCA、t-SNE、UMAP 之后,结果别乱扔,直接存进 reducedDims。

# 假设我们有PCA结果
pca_res <- matrix(runif(15), nrow = 5, ncol = 3)
colnames(pca_res) <- paste0("PC", 1:3)
rownames(pca_res) <- colnames(sce)

reducedDim(sce, "PCA") <- pca_res

# 再添加UMAP
umap_res <- matrix(runif(10), nrow = 5, ncol = 2)
reducedDim(sce, "UMAP") <- umap_res

4.4.2 sizeFactors —— 标准化缩放因子

这是 scran 质控和标准化流程的关键产物,专门用一个 slot 存储。后面做 normalization 时,scran 会自动读取这个值。

# 快速体验(后续章节会详解)
library(scran)
sce <- computeSumFactors(sce)
sizeFactors(sce)

我曾经过的坑:sizeFactors 和 counts 的长度必须与细胞数一致,且不能有 NA 或 0。如果某个细胞 sizeFactor 为 0,后面 logNorm 会出 NaN。检查一下!

4.5 SCE 的对象结构总览

为了让你更直观地理解这些组件之间的关系,我画了一张结构图。

SingleCellExperiment 结构示意 SingleCellExperiment 对象 assays counts, logcounts, ... colData 样本ID, 批次, QC状态 rowData 基因类型, 高变标记 metadata 实验参数, 版本信息 reducedDims PCA, UMAP, t-SNE sizeFactors 标准化缩放因子 说明: • assays 必须至少包含一个矩阵;colData / rowData / metadata 可选。 • reducedDims 只在降维后使用;sizeFactors 是 scran 标准化的产物。 • 所有组件通过行列索引自动对齐,增删细胞或基因时其它组件同步更新。

看到这张图应该能理解了吧。assays 是房子主体,colData 和 rowData 是挂在墙上的标签,metadata 是房子的说明书,reducedDims 和 sizeFactors 是后来添加的家具和装饰。

4.6 实战:构建一个完整的 SCE 对象

我们结合一个迷你数据集,把上面讲的串起来。

# 1. 原始数据
set.seed(100)
counts <- matrix(rnbinom(10000, mu = 10, size = 0.5), nrow = 200, ncol = 50)
rownames(counts) <- paste0("gene", 1:200)
colnames(counts) <- paste0("cell", 1:50)

# 2. 构建SCE
sce <- SingleCellExperiment(
    assays = list(counts = counts),
    colData = DataFrame(
        sample_id = rep(c("A", "B"), each = 25),
        cell_barcode = colnames(counts)
    ),
    rowData = DataFrame(
        gene_symbol = paste0("SYMBOL_", 1:200),
        is_mito = grepl("^MT", rownames(counts))
    ),
    metadata = list(
        created_at = Sys.time(),
        creator = "your_name"
    )
)

# 3. 计算一些质控指标并存入colData
library(scuttle)
sce <- addPerCellQCMetrics(sce, subsets = list(Mito = which(rowData(sce)$is_mito)))
sce$total_counts  # 每个细胞的文库大小
sce$detected      # 检测到的基因数
sce$subsets_Mito_percent  # 线粒体比例

# 4. 存一个降维结果(模拟)
pca_result <- matrix(rnorm(50*10), nrow = 50, ncol = 10)
colnames(pca_result) <- paste0("PC", 1:10)
reducedDim(sce, "PCA") <- pca_result

sce

重点回顾:

  • SCE 结构清晰,统一索引,扩展性强
  • assays 存表达数据,colData 存细胞注释,rowData 存基因注释,metadata 存实验级信息
  • reducedDims 和 sizeFactors 是 scran 工作流中的重要伙伴
  • 添加新数据时,直接 sce$新列 或 reducedDim(sce, "名称") 即可

记住一句话:好的数据结构,是高质量分析的第一步。我在实际项目中,从来不会让代码里出现两个以上彼此独立的 data.frame 来表达单细胞数据。统一用 SCE,所有下游分析、可视化、协作都顺畅很多。