数据预处理基础:标准化、归一化与缺失值处理
在做批次效应可视化之前,有一道绕不开的坎——数据预处理。说实话,很多同学拿到表达矩阵就急着画UMAP,结果图出来全是散的。嗯,问题往往出在数据没洗干净。
我个人习惯,任何降维之前,先问三个问题:数据量级一致吗?异常值处理了吗?缺失值填了吗?今天我们就把这几个问题一次性讲透。
核心原则:降维算法(PCA、UMAP)对输入数据的尺度和完整性极度敏感。批次效应本身就带来量纲差异,不预处理就等于拿放大镜看噪声。
标准化 vs 归一化:概念与区别
这两个词经常被混用,但本质不同。标准化(Standardization)把数据变成均值为0、标准差为1的分布;归一化(Normalization)把数据缩放到固定区间,通常是[0,1]。说白了,标准化保留分布形状,归一化只压缩范围。
| 特性 | 标准化 (Z-score) | 归一化 (Min-Max) |
|---|---|---|
| 公式 | $$x' = \frac{x - \mu}{\sigma}$$ | $$x' = \frac{x - x_{\min}}{x_{\max} - x_{\min}}$$ |
| 输出范围 | 无固定边界,理论无穷 | 严格[0,1]或[-1,1] |
| 对异常值敏感度 | 低(μ和σ受异常值影响) | 极高(极值决定缩放) |
| 常用场景 | PCA、线性模型 | 神经网络、距离计算 |
我在项目中遇到过,用PCA之前如果用Min-Max归一化,第一主成分往往被某个极端值带偏。所以降维前我默认都用Z-score,除非特征本身有明确上下界。
标准化:Z-score 方法
Z-score是最常见的标准化方式。方法是每个值减去均值,再除以标准差。这样处理后的数据服从标准正态分布(近似)。
import numpy as np
from sklearn.preprocessing import StandardScaler
# 假设 data 是 (n_samples, n_features) 的矩阵
data = np.random.rand(100, 50) # 示例数据
scaler = StandardScaler()
data_scaled = scaler.fit_transform(data)
# 验证:均值≈0,标准差≈1
print("Mean after scaling:", data_scaled.mean(axis=0)[:5])
print("Std after scaling:", data_scaled.std(axis=0)[:5])
小技巧:如果数据中有批次标签,建议先按批次分别计算均值标准差,再分别标准化——这叫“内部标准化”,可以有效削弱批次效应。我一般会在降维之前跑一次按批次Z-score,效果立竿见影。
归一化:Min-Max 缩放
Min-Max缩放把所有特征强制拉到[0,1]。公式很简单,但注意:它不改变分布形状,只是线性拉伸。万一数据里有离群点,大部分值会挤在一个很小的区间里。
from sklearn.preprocessing import MinMaxScaler
scaler_mm = MinMaxScaler()
data_mm = scaler_mm.fit_transform(data)
print("Min after scaling:", data_mm.min(axis=0)[:5])
print("Max after scaling:", data_mm.max(axis=0)[:5])
我曾经用Min-Max处理过基因表达数据,结果因为一个样本中的个别基因异常高表达,其他所有基因都被压缩到0.1以下。后来换成Z-score才正常。所以,你的数据有异常值吗?有的话就别用Min-Max。
缺失值处理方法
实际数据几乎没有完整的。缺失值(NaN)是降维的噩梦——PCA和UMAP都直接报错。怎么处理?
- 删除行/列:最简单粗暴。缺失率超过50%的特征或样本直接丢弃。我用过,但损失信息太多,不推荐做为主要手段。
- 均值/中位数填充:用该特征的均值或中位数填补缺失值。速度快,但会降低方差,影响后续分析。
- KNN 插补:利用相似样本的值来估计。效果较好,但计算量随样本量升高。
- 矩阵分解方法(如SVD/IterativeImputer):通过迭代或低秩逼近填补。适合高维组学数据,我偶尔在用。
import pandas as pd
from sklearn.impute import SimpleImputer, KNNImputer
# 创建含缺失值的DataFrame
df = pd.DataFrame([[1,2,None],[4,5,6],[7,None,9]], columns=['a','b','c'])
# 均值填充
mean_imp = SimpleImputer(strategy='mean')
df_mean = pd.DataFrame(mean_imp.fit_transform(df), columns=df.columns)
# KNN填充(k=3)
knn_imp = KNNImputer(n_neighbors=3)
df_knn = pd.DataFrame(knn_imp.fit_transform(df), columns=df.columns)
注意:填充缺失值前一定要区分“缺失”和“0”。很多表达数据里0就是真实检测不到,不等于缺失。我在项目里见过把0当成NaN删除的,结果丢了60%的基因。
实战中的小坑——避坑指南
我曾经有一批单细胞测序数据,批次效应很明显。我直接对整个矩阵做了全局Z-score,然后画PCA——批次聚成一团,但生物差异全没了。为什么?因为不同批次本身均值就有系统性偏移,全局标准化把批次差异当成了正常分布的中心。
正确的做法:先按批次分组,分别标准化,再合并。或者使用ComBat、Harmony等批次校正方法(这是我们后面章节的内容)。今天你只需记住:先拆分批次、再预处理、最后合并。
另一个坑是缺失值填充的顺序。我习惯先做缺失值处理,再做标准化/归一化。因为标准化计算均值和标准差时,如果有NaN,结果就是NaN。但反过来,先标准化再填充也勉强可行——只是填充的数值已经失去了原始量纲,解释起来别扭。
流程图:数据预处理的整体逻辑
下面这张图总结了从原始数据到降维分析的预处理流程。你可以把它贴在工位旁边,每次分析前看一眼。
这张图里,从原始数据往下走,第一步必须处理缺失值,然后才做标准化或归一化,最后才能扔给PCA/UMAP。顺序搞反了,结果就是一团糟。