第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])
4.5 缺失值处理决策流程图
下面这张图梳理了从发现缺失到最终处理的完整路径。我自己常年贴在工位上。
4.6 避坑指南
我曾经吃过一个亏:一批数据用均值填充后,模型AUC从0.92掉到0.85。当时我死活找不到原因,后来把填充逻辑反转——用中位数+分组(按工艺阶段)试了下,AUC回到0.91。差别就在那0.2个pH单位。
- 千万别在缺失率超过20%时硬填——填出来的分布就是统计幻觉。这种情况我建议直接标记为“数据不可信”,模型输出置为人工复核。
- 制药批次数据的时间戳必须保留。如果你把不同批次的点混在一起填充,等于破坏了工艺过程的时间序列结构。
- 审计追踪要覆盖缺失处理:监管要求每条数据从原始到清洗的变化都要可追溯。我在代码里一定加日志,记录每一处填充的原始值和替换值。
- 缺失值本身就是一种特征。我见过最好的做法是:补完值后,再新增一列“该位置原本缺失”,模型自己学要不要用这个信号。
嗯,总结下来就一句话:别把缺失值当垃圾扔掉,也别随便找个数字填上。把它当作一个信息源,用对方法,有时反而能帮你发现传感器异常、批次漂移这些更深层的问题。