数据预处理:缺失值处理与归一化基础
我做了这些年多批次质控项目,最深刻的体会就是:数据预处理的质量,直接决定了后续分析的成败。你想想看,如果原始数据里全是窟窿,或者不同批次的数据尺度差得离谱,后面用再牛的算法也救不回来。说实话,这部分工作看起来基础,但恰恰是坑最多的地方。
这一章咱们聚焦两个核心问题——缺失值怎么处理、数据怎么归一化。搞定了这两步,后面做 Rank Normalization 才能顺风顺水。
缺失值处理——绕不开的第一道坎
实际项目中,缺失值几乎是常态。仪器掉线、样本溶血、操作失误……原因五花八门。我在一个蛋白质组学项目里遇到过,某个批次因为离心机故障,一整批样本的 30% 的标记都丢了。当时如果不处理好,整个项目就得重做。
处理缺失值,第一步是搞清楚它怎么来的。一般分三种:
- 完全随机缺失(MCAR)——纯属意外,跟数据本身无关,比如试管打翻了。
- 随机缺失(MAR)——缺失跟其他变量有关,但跟它自己无关。比如高浓度的样本更容易被稀释,导致检测值缺失。
- 非随机缺失(MNAR)——缺失值本身就跟它的真实值有关。比如信号太弱检测不到,这种最麻烦。
处理方法我整理了一张表,平时项目里可以直接对着选:
| 方法 | 适用场景 | 优点 | 缺点 |
|---|---|---|---|
| 删除(行/列) | 缺失率低(<5%),且随机分布 | 简单粗暴,不影响分布 | 丢失信息,样本量骤减 |
| 均值/中位数填充 | 随机缺失,数据较稳定 | 保留样本量,计算快 | 降低方差,引入偏差 |
| KNN 插值 | 特征间相关性较强 | 利用相似样本信息,精度较高 | 计算量大,K 值难选 |
| 多重插补(MICE) | 缺失模式复杂,需要不确定性估计 | 统计性质好,保留变异 | 复杂度高,需要迭代 |
下面是我常用的快速检查和处理代码片段:
import pandas as pd
import numpy as np
# 读取多批次数据
df_batch1 = pd.read_csv("batch1.csv")
df_batch2 = pd.read_csv("batch2.csv")
# 检查缺失率
def check_missing(df, name):
miss_rate = df.isnull().mean() * 100
print(f"[{name}] 缺失率 > 5% 的特征:")
print(miss_rate[miss_rate > 5])
check_missing(df_batch1, "Batch1")
check_missing(df_batch2, "Batch2")
# 按批次独立填充中位数
for col in df_batch1.columns:
if df_batch1[col].isnull().any():
med = df_batch1[col].median()
df_batch1[col].fillna(med, inplace=True)
for col in df_batch2.columns:
if df_batch2[col].isnull().any():
med = df_batch2[col].median()
df_batch2[col].fillna(med, inplace=True)
归一化——让数据站在同一起跑线
不同批次的数据,量纲和分布往往不一样。有的批次信号强,数值范围 1000~10000,有的批次信号弱,只有 10~100。如果不做归一化,Rank Normalization 算出来的排名会被数值大的批次主导,这显然不是我们想要的。
为什么一定要做归一化?说白了就是三个原因:
- 消除量纲差异——让不同批次的特征有可比性。
- 稳定方差——避免高波动批次掩盖真实生物学差异。
- 满足算法前提——Rank Normalization 本身对尺度不敏感,但后续的聚类、PCA 等分析非常吃归一化。
我整理了三种最常用的方法,项目中按需选择:
| 方法 | 公式(简化) | 输出范围 | 适用场景 | 缺点 |
|---|---|---|---|---|
| Min-Max 归一化 | (x - min) / (max - min) | [0, 1] | 数据有明确上下界 | 对离群值极度敏感 |
| Z-score 标准化 | (x - mean) / std | 无界(均值为0) | 数据近似正态分布 | 受离群值影响较大 |
| 中位数绝对偏差(MAD) | (x - median) / MAD | 无界(中位数为0) | 存在离群值,分布偏态 | MAD 为0时需特殊处理 |
代码实现也很直接,我通常用 scikit-learn 配合 pandas 一起做:
from sklearn.preprocessing import StandardScaler, MinMaxScaler
from scipy.stats import median_abs_deviation
# 按批次独立 Z-score
scaler = StandardScaler()
df_batch1_scaled = scaler.fit_transform(df_batch1)
df_batch2_scaled = scaler.transform(df_batch2) # 注意:用 batch1 的参数
# 或者用 MAD 做抗离群值归一化
def mad_normalize(df):
med = df.median()
mad = median_abs_deviation(df, scale=1)
mad[mad == 0] = 1 # 避免除零
return (df - med) / mad
df_batch1_mad = mad_normalize(df_batch1)
df_batch2_mad = mad_normalize(df_batch2)
这里有个细节很多人会忽略:归一化的参数应该从参考批次(通常是质控样本所在的批次)中计算,然后应用到其他批次,而不是每个批次各算各的。否则批次间的分布会被强行拉齐,掩盖真实的批次效应。
预处理流程总览
下面这张图把缺失值处理和归一化的完整流程串起来了。我在每个项目中都会先画出这个流程,再动手写代码——方向对了,代码才不会跑偏。
我的实战建议
最后分享几个实战中沉淀下来的习惯,希望能帮你少走弯路:
- 先看分布,再动手。拿到数据第一件事不是写代码,而是画箱线图或者密度图,直观感受批次间的差异有多大。我个人习惯用 seaborn 的
boxplot快速扫一眼。 - 缺失值处理永远放在归一化之前。因为归一化用到的统计量(均值、中位数、标准差)会被缺失值污染,顺序不能乱。
- 保留原始数据副本。我每次都会把原始数据另存一份,预处理全部写在新的副本里。万一哪个环节出了问题,还能回退重来。
- 验证预处理效果。跑完归一化后,我会用 KS 检验或 Cohen's d 来评估批次间的分布差异是否减小。如果没效果,回头检查参数对齐方式。
好了,数据预处理这部分咱们就聊到这儿。说实话,这些操作看起来都不复杂,但真正在项目里每一步都做对的人,其实不多。把基础打扎实了,后面讲 Rank Normalization 的核心算法时,你才会觉得「原来如此」。