3、数据预处理基础:标准化、归一化、对数转换与缺失值处理
数据预处理,说白了就是给原始数据「洗澡」——洗掉噪声、拉平尺度、填上窟窿。我在做多组学项目时,这一块往往花掉40%以上的时间。你想想看,原始的表达量、甲基化β值、蛋白质丰度,尺度从0.001到100万都有,直接扔进模型?那基本就是灾难。
3.1 为什么数据预处理这么重要
多组学数据有三个让工程师头疼的地方:
- 尺度差异大:基因表达可能是几百到几十万,而代谢物浓度可能是pmol级别。模型里数值大的变量会主导梯度,小的直接消失。
- 分布歪斜严重:尤其转录组和微生物组,很多特征接近0,少数值极大,形成长尾。高斯假设的模型碰上这种分布基本失灵。
- 缺失率极高:质谱、芯片、测序都会产生大量NaN。我见过有的蛋白质组数据缺失超过30%,直接删掉?信息就全丢了。
我个人习惯在拿到原始数据后,先画一个分布图——histogram或QQ图。看一眼,心里就有数了:该取log还是该标准化,往往一眼就能判断。
3.2 标准化与归一化
这两个概念经常被混用,其实区别很清楚:
| 方法 | 公式 | 输出范围 | 保留分布形状 | 典型场景 |
|---|---|---|---|---|
| Z-score标准化 | (x - μ) / σ | 无界,约[-3,3] | 是(高斯化) | PCA、聚类、线性模型 |
| Min-Max归一化 | (x - min) / (max - min) | [0,1] | 是 | 神经网络、距离度量 |
| 鲁棒标准化 | (x - median) / IQR | 无界 | 否(抗异常值) | 有离群点的组学数据 |
记得有一回,我在处理TCGA的RNA-Seq数据,直接用Min-Max归一化。结果有个样本的某个基因异常高(实验污染),把整个特征的最大值拉高10倍,归一化后其他样本全都挤在0~0.1之间。后来换成鲁棒标准化,所有样本才正常。这就是坑。
代码实现很简单:
from sklearn.preprocessing import StandardScaler, MinMaxScaler, RobustScaler
# 假设 X 是样本×特征的矩阵
scaler_z = StandardScaler()
X_z = scaler_z.fit_transform(X)
scaler_mm = MinMaxScaler()
X_mm = scaler_mm.fit_transform(X)
scaler_robust = RobustScaler()
X_robust = scaler_robust.fit_transform(X)
3.3 对数转换
对数转换是我处理组学数据的「第一板斧」。为什么?因为很多生物数据服从对数正态分布,比如基因表达量。取对数之后,长尾被压缩,分布更接近高斯,聚类和差异分析的效果都会提升。
常用的对数底数:
- log2:倍数变化刚好对应1/2/4等整数,生物学意义直观。我偏好log2。
- log10:数量级差异明显,适合看量级变化。
- 自然对数ln:数学上方便求导,少数模型会用到。
遇到0值怎么办?常用做法是加一个伪计数(pseudocount),比如log2(x+1)或log2(x+0.5)。
import numpy as np
# log2转换,加伪计数避免log(0)
X_log = np.log2(X + 1)
# 也可以使用scikit-learn的FunctionTransformer
from sklearn.preprocessing import FunctionTransformer
log_transformer = FunctionTransformer(lambda x: np.log2(x+1), validate=True)
X_log = log_transformer.fit_transform(X)
3.4 缺失值处理
多组学数据缺失太常见了。我曾做过一个蛋白质组项目,30%的缺失率。直接删除样本?样本量不够。均值填充?会严重压低方差。
三种主流方法:
| 方法 | 原理 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|---|
| 均值/中位数填充 | 用该特征的均值(或中位数)替代缺失 | 简单快速 | 扭曲分布、降低方差 | 缺失率低(<5%) |
| KNN填充 | 找k个最相似的样本,用它们的均值加权填充 | 能利用样本间结构 | 计算量大、需标准化 | 缺失率中等(5%~20%) |
| 矩阵分解 | 将数据矩阵分解为低秩矩阵+噪声,用重建值填充 | 能捕捉全局结构,抗噪声 | 参数选择困难、不适合随机缺失 | 缺失率较高(>20%),有潜在低秩结构 |
均值填充:
from sklearn.impute import SimpleImputer
imputer_mean = SimpleImputer(strategy='mean')
X_filled = imputer_mean.fit_transform(X)
KNN填充:
from sklearn.impute import KNNImputer
# 注意:先标准化,否则距离被数值大的特征主导
imputer_knn = KNNImputer(n_neighbors=5, weights='distance')
X_knn = imputer_knn.fit_transform(X)
# 我的习惯:先标准化再KNN,填充完再反标准化回去
矩阵分解(以SVD为例):
from sklearn.decomposition import TruncatedSVD
import numpy as np
# 先近似填充0,再用SVD重建
X_temp = np.nan_to_num(X, nan=0.0)
svd = TruncatedSVD(n_components=20)
X_lowrank = svd.fit_transform(X_temp) @ svd.components_
# 然后将原缺失位置的NaN替换为重建值
mask = np.isnan(X)
X_filled = X.copy()
X_filled[mask] = X_lowrank[mask]
3.5 实战中的选择建议
- 转录组/微生物组:先log2转换,再鲁棒标准化(因为有异常高表达基因),缺失用KNN(k=5~10)。
- 甲基化β值:本身范围[0,1],Min-Max归一化即可,缺失用中位数填充(因为β值分布双峰)。
- 蛋白质组:先log2,再Z-score,缺失用SVD矩阵分解(往往有低秩结构)。
- 代谢组:如果数据稀疏,先Arcsinh转换(类似log但对0更友好),缺失用KNN或随机森林。我曾经试过用RF迭代填充,效果比KNN还好,但速度慢,看取舍。
最后提醒一句:所有预处理操作必须在同一个基准上跨样本一致。比如标准化参数来自训练集,测试集只应用不拟合。这道理简单,但我在很多项目代码里看到过反例——到处是bug。嗯,你核对一下自己的data pipeline吧。
以上就是我处理多组学数据时的「三步走」套路。没有银弹,但掌握这几种基础操作,再根据数据的特性灵活组合——相信我,你能省掉大把调试时间。