基因表达数据预处理:从raw count到干净特征

我经常会被问到这样一个问题:"RNA-seq数据拿过来,直接丢进模型行不行?"

说实话,不行。至少在我经手的几十个项目里,没有哪个是这么干的。

原始的表达数据,说白了就是一堆counts数。里面有各种噪声、批次效应、还有缺失值。你要是直接扔给神经网络,模型大概率会学偏。我刚开始做这个的时候也踩过坑,拿原始数据跑了一轮,结果调参调了三天,收敛效果依然很差。后来才发现,问题出在数据预处理阶段。

预处理不是可选项,它是模型训练的"地基"。地基不稳,上层建筑再漂亮也是白搭。

RNA-seq表达数据预处理流程 原始raw count数据 步骤1:缺失值处理 步骤2:归一化(Normalization) 步骤3:标准化(Standardization) 步骤4:批次校正(Batch Correction) 干净特征矩阵 常见问题 低表达基因过滤 文库大小差异 批次效应矫正 异常值处理 输出目标 均一化表达值 零均值单位方差 批次对齐 完整无缺失

缺失值处理:别让"NA"毁了你的模型

RNA-seq数据里出现缺失值,原因很多。可能是测序深度不够,某个基因恰好没被捕获;也可能是低表达基因在某个样本里压根检测不到。你想想看,如果有个基因在80%的样本里表达值都正常,就几个样本是NA,你直接去掉整个基因,信息损失太大了。

我个人的处理原则是:

  • 基因维度的缺失率 > 50%:直接删掉这个基因,因为它提供的信息太少。
  • 样本维度的缺失率 > 30%:删掉这个样本,可能存在实验质量问题。
  • 少量缺失(< 5%):用均值、中位数或者KNN插补。

我的习惯

对于深度学习的输入,我通常用基因在全部样本中的中位数来填充缺失值。为什么是中位数?因为表达数据偏态严重,均值容易被极端值拉偏。中位数更稳健。

避坑提醒

千万不要在插补之前做标准化!我曾经见过一个错误案例,先用Z-score标准化,发现缺失值被算成了0,插补完整个分布都扭曲了。顺序搞反,后果很严重。

归一化:消除文库大小差异

归一化解决的是"文库大小不同"的问题。同一个基因,在A样本里测了100万条reads,在B样本里测了200万条。如果不做归一化,模型会误以为B样本里这个基因表达量翻倍了。其实只是测序深度不同而已。

常用的方法有几种,我整理了一张表:

方法 原理 适用场景 我推荐的场景
CPM (Counts Per Million) 每个基因的count除以总count,再乘1e6 快速粗略比较 初筛、探索性分析
TPM (Transcripts Per Million) 考虑基因长度,先除以基因长度再归一化 跨基因比较 需要比较不同基因的表达量
FPKM/RPKM 类似TPM,早期常用,现已逐渐淘汰 历史遗留项目 尽量别用,有偏差
DESeq2 median of ratios 基于每个基因与几何均值的比值的中位数 差异表达分析、深度学习 最推荐
TMM (edgeR) 基于 trimmed mean of M-values 差异表达分析 适合与edgeR配合

我个人习惯用DESeq2的median of ratios方法。原因很简单:它对高表达和低表达基因的偏差校正最鲁棒。尤其是当你的数据里存在大量低表达基因时,CPM这种粗暴方法会让方差变得非常大。

实操命令

在R里,用DESeq2做归一化其实就一行代码:

library(DESeq2)
dds <- DESeqDataSetFromMatrix(countData = counts,
                              colData = metadata,
                              design = ~ condition)
dds <- estimateSizeFactors(dds)
normalized_counts <- counts(dds, normalized = TRUE)

这里的关键是estimateSizeFactors。它会计算出每个样本的"size factor",相当于一个缩放系数。归一化后的counts,就可以用来做后续的机器学习任务了。

小技巧

拿到normalized_counts之后,我习惯先取log2变换:log2(normalized_counts + 1)。这能让偏态分布变得更接近正态,对模型收敛很有帮助。

标准化:让所有基因站在同一起跑线

标准化和归一化不是一回事。归一化是消除文库大小差异,标准化是让每个基因的表达值分布对齐——通常是零均值、单位方差。

为什么需要标准化?你想想看,有的基因表达量很高(比如管家基因),均值为几千甚至上万;有的基因表达量很低,均值不到10。梯度下降的时候,高表达基因的梯度会主导更新方向,低表达基因的信息就被淹没了。

标准做法是Z-score:

from sklearn.preprocessing import StandardScaler

scaler = StandardScaler()
X_scaled = scaler.fit_transform(normalized_log2_counts)

标准化之后,每个基因的均值为0,标准差为1。模型就不会"偏袒"高表达基因了。

注意分割

标准化参数(均值和标准差)一定要在训练集上计算,然后用同样的参数去变换验证集和测试集。千万别在全体数据上fit_transform。我早年犯过这个错误,导致模型在验证集上的表现虚高,上线后直接崩了。

批次校正:整合多个实验的数据

实际项目中,你很少能在一个批次里收完所有样本。不同时间、不同操作员、不同试剂批次,都会引入系统性的偏差。这种偏差叫"批次效应"。

有一次我做一个项目,数据来自三个不同实验室。不校正批次的话,样本在PCA图上直接按实验室分成了三群。模型学到的不是生物学差异,而是实验室差异。

常用的批次校正方法:

  • ComBat (sva包):基于贝叶斯框架,是目前最流行的方法之一。对已知批次效果很好。
  • limma (removeBatchEffect):线性回归的方法,简单高效。
  • Harmony:基于软聚类和迭代校正,适合大规模单细胞数据,但也可以用于bulk RNA-seq。
  • scVI / scGen:基于变分自编码器的深度学习方法,适合复杂场景。

R代码(ComBat)

library(sva)
adjusted_counts <- ComBat(dat = log2_normalized_counts,
                          batch = batch_labels,
                          mod = model.matrix(~ condition),
                          par.prior = TRUE,
                          prior.plots = FALSE)

注意mod参数里要包含你的生物学变量,这样做是为了避免校正掉真正的生物学差异。

我的经验

批次校正不是万能的。如果批次和生物学分组完全共线(比如所有case在批次1,所有control在批次2),那谁也校正不了。这种情况我建议你找实验设计的问题,别在数据分析上硬扛。

预处理检查清单

每次我拿到一批RNA-seq数据,都会按下面的清单过一遍:

  1. 缺失值检查:统计每个基因和每个样本的缺失率,决定删除还是插补。
  2. 低表达基因过滤:通常我会去掉在超过80%的样本中count < 10的基因。
  3. 归一化:用DESeq2的median of ratios方法,得到normalized counts。
  4. log2变换:log2(normalized_counts + 1),稳定方差。
  5. 标准化:Z-score,每个基因零均值单位方差。
  6. 批次校正:如果存在多个批次,用ComBat或者limma处理。
  7. 最终检查:PCA可视化,看看样本是否按生物学分组聚集,而不是按批次或技术变量聚集。

一句话总结

预处理的目的,就是让数据干净、可比、无偏。你花在预处理上的每一个小时,都会在模型训练和调参阶段加倍还给你。