数据预处理:缺失值处理与归一化基础

我做了这些年多批次质控项目,最深刻的体会就是:数据预处理的质量,直接决定了后续分析的成败。你想想看,如果原始数据里全是窟窿,或者不同批次的数据尺度差得离谱,后面用再牛的算法也救不回来。说实话,这部分工作看起来基础,但恰恰是坑最多的地方。

这一章咱们聚焦两个核心问题——缺失值怎么处理、数据怎么归一化。搞定了这两步,后面做 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)
经验之谈:如果某个特征在单个批次中缺失率超过 80%,我建议直接删掉这个特征,强行填充只会引入噪声。少一个特征比塞进一堆垃圾数据要安全得多。

归一化——让数据站在同一起跑线

不同批次的数据,量纲和分布往往不一样。有的批次信号强,数值范围 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时需特殊处理
核心要点:Rank Normalization 本质上是一种“排序后映射到均匀分布”的操作,它本身不依赖原始数值的尺度。但如果前面归一化做得不好,离群值会把排序结果拉偏,导致排名失真。所以归一化不是给 Rank Normalization 用的,而是给“排名前的数据清洗”用的。

代码实现也很直接,我通常用 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)

这里有个细节很多人会忽略:归一化的参数应该从参考批次(通常是质控样本所在的批次)中计算,然后应用到其他批次,而不是每个批次各算各的。否则批次间的分布会被强行拉齐,掩盖真实的批次效应。

我曾经踩过的坑:在一个代谢组学项目中,我让每个批次独立做 Z-score,结果本来很明显的批次效应全部消失了。后来排查才发现,归一化参数应该对齐到同一个参考批次,而不是各自为政。那一次让我多花了整整两周重新处理数据。

预处理流程总览

下面这张图把缺失值处理和归一化的完整流程串起来了。我在每个项目中都会先画出这个流程,再动手写代码——方向对了,代码才不会跑偏。

数据预处理核心流程 原始多批次数据 批次1, 2, ..., n 缺失值识别 评估缺失模式与比率 缺失值处理 删除 / 填充 / 插值 归一化 Min-Max / Z-score / MAD 预处理完成 质控就绪数据 关键原则 ① 缺失值处理和归一化必须按批次独立进行,参数从参考批次中计算。 ② 预处理完成后,务必检查每个批次的分布形态是否对齐,再进入 Rank Normalization 环节。

我的实战建议

最后分享几个实战中沉淀下来的习惯,希望能帮你少走弯路:

  • 先看分布,再动手。拿到数据第一件事不是写代码,而是画箱线图或者密度图,直观感受批次间的差异有多大。我个人习惯用 seaborn 的 boxplot 快速扫一眼。
  • 缺失值处理永远放在归一化之前。因为归一化用到的统计量(均值、中位数、标准差)会被缺失值污染,顺序不能乱。
  • 保留原始数据副本。我每次都会把原始数据另存一份,预处理全部写在新的副本里。万一哪个环节出了问题,还能回退重来。
  • 验证预处理效果。跑完归一化后,我会用 KS 检验或 Cohen's d 来评估批次间的分布差异是否减小。如果没效果,回头检查参数对齐方式。
一个小技巧:当你不确定用哪种归一化方法时,先试试 MAD 归一化。它对离群值不敏感,而且跟 Rank Normalization 的思路更接近——都基于中位数和稳健统计量。我自己在 80% 的项目里最终都选了这个方案。

好了,数据预处理这部分咱们就聊到这儿。说实话,这些操作看起来都不复杂,但真正在项目里每一步都做对的人,其实不多。把基础打扎实了,后面讲 Rank Normalization 的核心算法时,你才会觉得「原来如此」。