4. DESeq2对象创建:DESeqDataSetFromMatrix

做差异表达分析,第一步就是把你的数据喂给DESeq2。很多人一上来就跑DESeq(),结果报错一堆——八成是对象没建对。我个人习惯先从DESeqDataSetFromMatrix这个函数开始,它就像搭乐高的底盘,底歪了后面全歪。

说白了,这个函数就是把三样东西绑在一起:

  • 原始计数矩阵(count matrix)
  • 样本信息(colData)
  • 实验设计公式(design formula)

嗯,听起来简单?但坑不少。我带过一个项目,合作方直接把TPM矩阵扔进去,结果归一化全乱了——因为DESeq2只认整数计数,不接受浮点归一化值。所以先确认你的输入是整数,而且没经过任何文库规模校正。

4.1 函数签名与参数

看看API长什么样:

DESeqDataSetFromMatrix(
  countData,
  colData,
  design,
  tidy = FALSE,
  ignoreRank = FALSE,
  ...
)

核心参数就三个,后面几个我几乎没用过默认值就够了。但有一个细节:tidy = TRUE时,countData第一列是基因ID,后面才是样本列。我个人不建议开这个选项,容易搞混列索引,显式指定行列名更安全。

4.2 输入数据的格式要求

表格列一下关键点,项目里吃过亏的地方我标红了:

输入对象要求常见陷阱
countData整数矩阵或数据框,行=基因,列=样本出现NA、负数、浮点数(即使是小数也报错)
colData数据框,行=样本,列=实验变量行名必须与countData列名严格一致,顺序可以不同
design公式,如 ~ condition因子水平顺序会影响基线,默认按字母序

记得有一次,我同事把colData的行名写成了样本barcode但countData的列名是sample_id,两个名字不匹配,函数直接报错“assay colnames not identical to colData rownames”。排查了一个小时。后来我养成了习惯:创建对象前先跑all(colnames(countData) == rownames(colData)),返回TRUE再往下走。

4.3 标准创建流程

我一般按这个步骤来,几乎不会出问题:

  1. 检查计数矩阵:mode(counts) == "integer",没有缺失值,基因名不能重复。
  2. 对齐样本顺序:虽然DESeq2内部会匹配,但手动对齐能减少一个警告信息。
  3. 设置因子水平:用factor()显式指定control组在前,这样结果里的log2 fold change就是处理组相对于对照组。
  4. 构建对象:一行代码搞定。

来看一个实际例子。假设你的数据来自一个敲除实验,有野生型(WT)和突变型(KO),每个3个重复:

# 1. 计数矩阵(6个样本,1000个基因)
counts_matrix <- read.csv("counts.csv", row.names = 1)
# 确保是整数
counts_matrix <- round(as.matrix(counts_matrix))

# 2. 样本信息
colData <- data.frame(
  condition = c("WT","WT","WT","KO","KO","KO"),
  row.names = colnames(counts_matrix)
)

# 3. 显式指定因子水平(基线=WT)
colData$condition <- factor(colData$condition, levels = c("WT","KO"))

# 4. 创建DESeqDataSet对象
dds <- DESeqDataSetFromMatrix(
  countData = counts_matrix,
  colData = colData,
  design = ~ condition
)

好了,你现在有了一个dds对象。可以用class(dds)确认一下,它同时继承自RangedSummarizedExperiment,所以有很多好用的槽位。

4.4 对象内部结构

创建成功后,对象里装了什么?我画了个简图帮你理解:

DESeqDataSet 对象结构 DESeqDataSet countData colData design SummarizedExperiment 继承 assays:counts colData metadata 整数计数矩阵 实验变量 设计公式 三个输入 → 一个对象,内部封装为 SummarizedExperiment 结构

图中可以看到,最终对象是一个RangedSummarizedExperiment,里面装着count矩阵、样本注释数据框,以及设计公式。这些信息后续的DESeq()、results()都会用到。

4.5 常见坑与避坑指南

我曾经踩过的坑:

  • 计数矩阵里有基因名为NA,DESeq2会报错“rownames cannot contain NA”。用is.na(rownames(counts))查一下。
  • colData的因子顺序没定义,导致results()出来log2FC符号完全反了——因为默认以字母序第一个作为基线。
  • design公式里用了冒号写交互项,但样本量不够时DESeq2会报“design has only one level”。小实验别强求交互。

一个小技巧:

创建对象后立刻跑colnames(dds)和colData(dds)看看匹配情况。我常在DESeqDataSetFromMatrix后面加一行dds <- estimateSizeFactors(dds)来先估算size factor——如果报错“every gene contains at least one zero”,说明你的矩阵里有一半以上的零,需要提前过滤低表达基因。

4.6 为什么不用其他构造函数?

你可能会问,DESeq2还提供了DESeqDataSet、DESeqDataSetFromTximport、DESeqDataSetFromHTSeq。但DESeqDataSetFromMatrix是最通用的——它接受任何整数矩阵,不管是从featureCounts、STAR还是自制pipeline来的。我项目里对接不同来源的数据,全用这个函数,只改读取方式,后处理统一。

举个例子,如果你用tximport导入salmon定量结果,可以用DESeqDataSetFromTximport,但那个函数底层最终还是调用了DESeqDataSetFromMatrix。所以理解核心的Matrix版本,其他都是衍生。

4.7 创建后必须做的检查

不是建完对象就万事大吉了。我建议至少确认三点:

# 1. 查看样本统计
summary(colData(dds))

# 2. 查看每个基因的中位数计数
median_count <- apply(counts(dds), 1, median)
summary(median_count)  # 很多基因中位数为0?需要过滤

# 3. 是否有大量全部为0的基因
table(apply(counts(dds), 1, function(x) all(x == 0)))
# 如果太多,考虑去除,否则影响后续拟合

我曾经接手一个单细胞数据转伪bulk的表达矩阵,没做低表达过滤,结果DESeq2跑了一天没跑完。后来把rowSums(counts(dds) >= 10) >= 3的基因留下,速度提升了10倍。嗯,这个经验值钱了。

4.8 与edgeR对比:从数据结构角度

虽然这章讲DESeq2,但我觉得有必要提一嘴edgeR的构造方式,免得切换工具时晕头转向。edgeR用DGEList(counts, group)创建对象,结构更扁平,没有design formula的概念——它是在后续的model.matrix里指定。而DESeq2把design直接绑定在对象里,后面不能随便改(除非重建)。所以如果你还在实验设计阶段举棋不定,我个人建议先用DGEList做探索性分析,确定了模型再转DESeq2也行。

但话说回来,一旦你决定用DESeq2,就踏踏实实把DESeqDataSetFromMatrix这一步做扎实。对象建好了,后面就是一句dds <- DESeq(dds)的事。别再回头改计数矩阵了,相信我——data integrity比什么都重要。