第4章 数据清洗与预处理错误:缺失值处理不当

数据清洗里,缺失值处理看着简单,坑却最多。我见过多个AI批次质控项目,模型训练时指标漂亮,一上线就崩——查到最后,全是缺失值埋的雷。

说白了,制药行业的数据不是实验室理想环境。传感器掉线、人工录入漏填、数据传输丢包……处处都可能产生缺失值。你用什么方式补,补完有没有破坏原始分布,都直接影响模型对批次质量的判断。

4.1 三个常见要命的错误

我习惯把缺失值处理错误归成三类,每一类我都踩过,所以特别想跟你聊聊。

  • 暴力删除(Listwise Deletion):只要某行有一个空值,整行扔掉。批量少时,样本量骤降,模型学不到真实分布。我曾见过一个项目,原数据5000批,删除后只剩800批,模型直接过拟合到少数模式上。
  • 无脑均值填补:用整列均值填所有空缺。连续三个批次pH值缺失,全填7.0——等于把波动信息压平了。模型会误以为这几批都“稳定在7.0”,实际可能早就漂移了。
  • 忽略缺失机制:完全随机缺失(MCAR)、随机缺失(MAR)、非随机缺失(MNAR),处理方法完全不同。制药批次数据里,设备故障导致的缺失往往是MNAR——补错了等于造假。
警告:制药批次数据中的缺失值不能简单当作统计问题看待。它背后可能是设备校准失效、操作偏差、环境突变。补值前一定要去现场确认根因。

4.2 案例拆解:pH值连续缺失导致模型误判

背景:某原料药批次质控AI模型,输入特征包括温度、压力、pH、搅拌速率等。模型在测试集上准确率97%,上线第一周就报了两批假阳性——明明合格的批次被判为不合格。

我的排查过程:我调出原始记录,发现那两批的pH传感器在前15分钟全部掉线,数据保存为NaN。研发同事用了最简单的“均值填补”,把前140批的pH平均值填了进去。

你想想看,pH在发酵过程中是动态变化的。第一小时通常从6.8降到6.2,均值7.1完全不符合真实曲线。模型学到的是“pH=7.1附近属于正常波动”,而实际补进去的值和真实值差了0.9个pH单位——于是模型认为异常,亮红灯。

根因:缺失模式是非随机缺失(MNAR),传感器掉线本身就是质量风险的信号。直接均值填补掩盖了异常,导致模型失效。

解决方案:改用时间序列插值+上游工艺参数关联的缺失值填充模型。对连续缺失超过5分钟的数据,直接标记为“传感器异常”特征,而不是硬填。

教训:
• 缺失值不只是一个技术问题,它还携带信息——要不要保留“缺失”本身作为一个特征?
• 制药批次数据里,时间顺序不能乱。均值填充会把时间结构打碎。

4.3 错误与正确做法对比

处理方式 典型错误 正确做法 适用场景
删除缺失行 缺失率>5%仍直接删除,样本量骤减 重复实验中可删除;关键指标缺失不可删 缺失率低(<5%)且MCAR
均值/中位数填补 所有特征统一填均值,忽略分布 仅对MCAR数据可用,且需分组填补 轻度缺失,变量分布对称
前向/后向填充 跨批次填充,忽略批次边界 同一批次内部按时间顺序填充 时间序列数据,缺失不超过3个点
模型预测填补 用全特征预测缺失值,引入多重共线性 仅用不相关外部特征预测,保留误差 缺失率较高,且变量间有物理关系
标记缺失为独立特征 只填值不标记,丢失缺失信息 新增“是否缺失”二元特征 非随机缺失(MNAR)必须用

4.4 代码示例:pandas中的正确与错误姿势

这是我在项目中见过最多的错误写法——简单粗暴,然后埋雷。

# ❌ 错误做法:全删除 + 全局均值填充
import pandas as pd
df = pd.read_csv('batch_data.csv')
df.dropna(inplace=True)   # 5000批变成800批
df['pH'].fillna(df['pH'].mean(), inplace=True)   # 全部填7.1

正确的做法应该先分析缺失模式,再对症下药。我一般在项目里这样写:

# ✅ 正确做法:先分析缺失模式,再分步处理
import pandas as pd
import numpy as np
from sklearn.experimental import enable_iterative_imputer
from sklearn.impute import IterativeImputer

# 1. 标记缺失位置作为新特征
for col in ['pH', 'temperature', 'pressure']:
    df[f'{col}_missing'] = df[col].isna().astype(int)

# 2. 按批次分组做时间序列插值(同一批内按时间排序)
df.sort_values(['batch_id', 'time_point'], inplace=True)
df['pH'] = df.groupby('batch_id')['pH'].transform(
    lambda x: x.interpolate(method='linear', limit=3)
)

# 3. 仍缺失的用IterativeImputer(多变量填充)
imputer = IterativeImputer(max_iter=10, random_state=42)
cols_to_impute = ['pH', 'temperature', 'pressure']
df[cols_to_impute] = imputer.fit_transform(df[cols_to_impute])
技巧:制药批次数据经常有重复测量(比如每隔30秒记录一次)。同批内的缺失可以用线性插值先补,补不了的再用模型。我习惯保留一个“缺失时长”特征,防止模型忽略掉线信息。

4.5 缺失值处理决策流程图

下面这张图梳理了从发现缺失到最终处理的完整路径。我自己常年贴在工位上。

缺失值处理决策流程(制药批次数据) 发现缺失值 缺失率 < 5% ? (同时检查缺失模式) 是(MCAR) 否(MAR/MNAR) ① 删除少量缺失行 ② 线性插值(同批内) ① 时间序列模型填充 ② 新增缺失标记特征 处理后仍有缺失 ? (特别是关键工艺参数) 是 否 标记为“数据缺失” 模型输出置为“需要复核” 进入模型训练 保留缺失标记特征 输出结果 每条批次记录标注缺失处理方式 + 可审计日志 每个处理步骤都需记录到MES系统,便于审计追溯

4.6 避坑指南

我曾经吃过一个亏:一批数据用均值填充后,模型AUC从0.92掉到0.85。当时我死活找不到原因,后来把填充逻辑反转——用中位数+分组(按工艺阶段)试了下,AUC回到0.91。差别就在那0.2个pH单位。

  • 千万别在缺失率超过20%时硬填——填出来的分布就是统计幻觉。这种情况我建议直接标记为“数据不可信”,模型输出置为人工复核。
  • 制药批次数据的时间戳必须保留。如果你把不同批次的点混在一起填充,等于破坏了工艺过程的时间序列结构。
  • 审计追踪要覆盖缺失处理:监管要求每条数据从原始到清洗的变化都要可追溯。我在代码里一定加日志,记录每一处填充的原始值和替换值。
  • 缺失值本身就是一种特征。我见过最好的做法是:补完值后,再新增一列“该位置原本缺失”,模型自己学要不要用这个信号。
我的小习惯:每次处理完缺失,我会做一次分布对比——原始非缺失数据的分布 vs 填充后完整数据的分布。如果KS检验p值小于0.05,说明填充改变了分布,必须调方法。这招帮我避免了三个项目事故。

嗯,总结下来就一句话:别把缺失值当垃圾扔掉,也别随便找个数字填上。把它当作一个信息源,用对方法,有时反而能帮你发现传感器异常、批次漂移这些更深层的问题。