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)
注意:标准化和归一化一定要先分割训练/测试集,然后用训练集的参数(μ、σ、min、max)去转换测试集。否则会数据泄露——测试集的信息跑到训练里,模型评估就失效了。

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)
经验:对于微生物组相对丰度数据,很多值本身就是很小的分数(0.0001级别),直接取log会得到很大的负数。我建议先乘以一个缩放因子(比如1000),再取log2。或者改用arcsin平方根转换,对稀疏数据更友好。

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]
避坑指南:千万不要把缺失值填成0后再做标准化,因为0被标准化后会变成负值(均值被往0拉),造成系统性偏移。正确顺序:先填充,后标准化。我吃过一次亏,后来检查分布才发现。

3.5 实战中的选择建议

  • 转录组/微生物组:先log2转换,再鲁棒标准化(因为有异常高表达基因),缺失用KNN(k=5~10)。
  • 甲基化β值:本身范围[0,1],Min-Max归一化即可,缺失用中位数填充(因为β值分布双峰)。
  • 蛋白质组:先log2,再Z-score,缺失用SVD矩阵分解(往往有低秩结构)。
  • 代谢组:如果数据稀疏,先Arcsinh转换(类似log但对0更友好),缺失用KNN或随机森林。我曾经试过用RF迭代填充,效果比KNN还好,但速度慢,看取舍。

最后提醒一句:所有预处理操作必须在同一个基准上跨样本一致。比如标准化参数来自训练集,测试集只应用不拟合。这道理简单,但我在很多项目代码里看到过反例——到处是bug。嗯,你核对一下自己的data pipeline吧。

多组学数据预处理流程 原始组学数据 分布形状判断 对数转换 (log2) 标准化/归一化 缺失值处理 加伪计数 → log2(x+1) Z-score / Min-Max / Robust 均值/KNN/矩阵分解 预处理后的数据 注意:标准化与缺失值处理的顺序 → 先填充后标准化(避免填充值被标准化扭曲) 对数转换通常放在最前面,以便后续标准化对尺度更敏感

以上就是我处理多组学数据时的「三步走」套路。没有银弹,但掌握这几种基础操作,再根据数据的特性灵活组合——相信我,你能省掉大把调试时间。