
上周帮一位做工业设备故障预测的朋友看模型他的 XGBoost 二分类模型 AUC 卡在 0.72 上不去换了三种网络结构、调了两轮学习率、连特征重要性都重排了依然纹丝不动。我把他三万多条训练记录拉出来扫了一遍问题全在数据里有两千多条重复采样记录压力那一列单位一会儿是 MPa 一会儿是 kPa还有一整段停机时长是负数。花了一下午把数据预处理整条链路重做模型一行没改AUC 直接到 0.89。这件事之后我更加确信那句话——数据预处理不是训练前的杂活它直接决定了模型能学到的上限。这篇内容我想聊的就是数据预处理这件事本身它包含哪些环节、每个环节为什么这么做、参数怎么算、不同模型对预处理的要求差在哪、以及我在实操中踩过的那些坑。不管你是刚接触机器学习、在实训平台上做 pandas 数据预处理练习的新手还是已经能独立训练 Transformer、UNet 这类模型、但总觉得效果差一口气的从业者下面的内容应该都能对上号。1. 预处理决定了模型的天花板先把整体思路捋清楚1.1 一个真实的对比同样的模型预处理前后差多少我习惯把数据预处理分成四件事清洗、转换、降维、特征构造。这四个词听起来像教科书目录但真正决定效果的往往不是做没做而是顺序对不对、参数从哪来。上面那个设备预测的例子我做的操作其实很朴素。第一步把重复样本按设备编号加时间戳去重去掉 2147 条第二步统一压力单位全部换算成 kPa第三步把停机时长里的负值按业务规则修正为 0因为负值在物理上不成立它其实是传感器在重启瞬间写进去的脏数据第四步对偏态严重的振动幅值做了对数变换再做标准化。四步做完特征分布从拖着一条长尾巴变成接近对称树模型分裂时的候选切分点质量明显提升这就是 AUC 跳上去的直接原因。反过来我也见过反例。有个朋友做照片修复模型直接把手机拍的图丢进去训练网络是改进过的 UNet结构没问题但训练 loss 震荡得厉害。后来发现他的训练图片尺寸从 320×240 到 4000×3000 都有而且没做统一的归一化像素值有的在 0 到 255有的已经被别的脚本除过 255。输入尺度不统一等于给每一张图随机换了一把尺子模型当然学不稳。同一批数据统一缩放到 512×512、像素除 255、加一点随机水平翻转做增强loss 曲线立刻平顺了。这两个例子指向同一个结论模型结构和超参是上层建筑预处理是地基地基歪了楼上怎么装修都没用。1.2 预处理流水线的四个环节与顺序为什么不能乱很多人做预处理是按想起什么做什么的顺序来的这很容易出问题。我一般按下面这个顺序走理由写在每一项后面清洗去重、修异常、补缺失、统一单位与编码。必须先做因为后面所有的统计量均值、方差、分位数都会被脏数据污染。转换类型转换、日期拆解、数值缩放、分布调整。放在清洗之后这时候算出来的均值方差才是可信的。特征构造交叉项、聚合统计、滑动窗口特征、文本与图像的额外表示。放在转换之后因为构造过程经常会依赖已经标准化的量。降维与选择PCA、特征筛选、相关性剔除。放在最后因为它要用到完整特征集的信息。顺序里最容易出错的是缩放和划分数据集的先后。正确做法是先划分训练集和验证集只在训练集上 fit 缩放器再 transform 到验证集。如果先对全量数据做标准化再划分验证集的均值和方差信息就通过缩放参数泄漏进了训练过程线下指标会虚高上线就掉。这个坑我见过太多次后面第 6 章会给一个用 sklearn Pipeline 自动规避的写法。提示任何需要计算统计量的预处理步骤标准化、缺失值填充、目标编码、PCA都必须遵守只在训练集 fit这条铁律它比选什么模型重要得多。另外要说一句关于滑动窗口滤波这类时序预处理。做温度控制系统辨识、FPODT 模型参数拟合时很多人会先对原始信号做滑动平均去掉高频噪声。这一步本身没问题但窗口长度不能拍脑袋定窗口太长会把真实的阶跃响应抹平导致辨识出来的时间常数偏大窗口太短则噪声还在。我一般的做法是先看信号的采样周期窗口取采样周期的 5 到 10 倍再用残差自相关图确认噪声是否已经接近白噪声。2. 数据清洗把脏数据挡在训练集门外2.1 缺失值删、填、还是留一个标记位缺失值处理没有标准答案只有哪种代价你更能接受。我常用的判断逻辑是这样先算每一列的缺失比例再判断缺失是否随机。缺失比例缺失类型我通常的处理方式理由小于 5%随机缺失中位数或众数填充影响小填充成本最低5% 到 30%随机缺失模型填充KNN、迭代填充或加缺失指示列保留信息量同时让模型知道这里缺过大于 30%随机缺失考虑直接丢弃该列填充引入的偏差可能大于信息收益任意比例非随机缺失必须加缺失指示列慎重填充缺失本身可能携带强信号缺失本身携带信号这句话不是玄学。举个真实场景某类传感器在设备过载时会短暂掉线那么这一列缺失就和设备过载强相关直接把它填成均值反而把这个信号抹掉了。所以我处理缺失值时的默认动作是——先加一列 is_missing 的 0/1 标记再考虑填充多这一列几乎不增加成本但经常能救回一部分效果。填充方法里中位数比均值更稳因为均值本身会被异常值拉动对于明显有前后依赖的时序数据用前向填充ffill比全局中位数合理得多因为传感器读数在短时间内是连续的。注意用前向填充时一定要配合时间排序。如果数据是按设备编号排的直接 ffill 会把上一台设备的最后一条读数填到下一台设备的第一条上这种错误非常隐蔽。2.2 异常值什么时候该删什么时候必须留新手最容易犯的错是看到离群点就删。我一般先问三个问题这个值物理上可能吗它是录入错误还是真实事件删掉它会不会把正样本删光物理不可能的值必须处理比如负的停机时长、超过量程上限的压力读数、年龄 300 岁。这类值我按业务规则修正或置为缺失而不是直接删行因为同一行的其他列可能还是有效的。物理可能但极端的值要谨慎。做设备故障预测时真正的故障瞬间往往就是极端值你要是一股脑按 3σ 规则删掉等于把最珍贵的正样本扔了。这时候更合适的做法是分位数截断winsorize而不是删除把超过 99 分位或低于 1 分位的值压到边界上既限制了它对线性模型和神经网络的破坏力又保留了这里出过极端情况的信息。判断异常值的手段我常用三种按可靠性排序业务规则最可靠、IQR 箱线图规则对偏态数据比 3σ 稳、孤立森林等无监督方法适合高维但结果需要人工复核。3σ 规则的前提是数据近似正态而真实业务数据十有八九是长尾的硬套 3σ 会误杀大量正常样本这一点做工业时序数据的人应该有体会。2.3 重复值与一致性校验最容易被忽略的一步重复值不只是整行完全一样。真正麻烦的是实体级重复同一个用户被录入了两次但手机号格式不同同一台设备因为编号规则变更产生了两个 ID。这类重复合并后模型会以为这两个样本独立实际上它们的标签高度相关导致验证集指标虚高。一致性校验我固定会做这几件事检查类别列的取值集合有没有北京北京市Beijing这种同义异形检查日期列的格式是否统一有没有混着时间戳和字符串检查数值列的量纲同一物理量在不同表里单位是否一致检查主键在表内是否唯一、在关联表间是否能完全匹配。这些检查用几行 pandas 就能跑完但能省下后面几个小时的排查时间。我还习惯在清洗完成后做一次数据契约记录把每列的期望类型、取值范围、允许的类别集合写成一个字典存下来。这样下次数据更新时跑一遍校验脚本就能立刻发现上游哪张表改了格式。这个习惯是从一次事故里养成的——上游同事把某个字段从整型改成字符串模型没报错只是静默地把这一列当成了类别特征效果掉了一大截我们查了两天才定位到。3. 数据转换标准化、归一化与分布调整3.1 标准化和归一化的选择与手算过程这两个词经常被混用我用一句话区分标准化Z-score改变的是分布的位置和尺度归一化Min-Max改变的是取值范围。标准化的公式是 z (x - μ) / σ其中 μ 是均值σ 是标准差。举个数某列振动幅值的训练集均值是 42标准差是 8样本值 58 标准化后就是 (58 - 42) / 8 2.0。归一化的公式是 x (x - min) / (max - min)同样的样本如果训练集最小 10、最大 74归一化后是 (58 - 10) / (74 - 10) 0.75。选哪个我一般看三件事判断维度选标准化选归一化算法类型线性回归、逻辑回归、SVM、PCA、神经网络需要固定输入范围的场景如图像像素、某些距离度量数据分布近似正态或有少量极端值边界明确且稳定如评分 0 到 100新数据风险较小新样本超出范围也不会越界较大新样本超出训练 min/max 就会跑出 0 到 1 区间神经网络特别是深层结构我基本默认用标准化因为权重初始化、BatchNorm 这类机制都假设输入接近零均值单位方差。图像数据是例外直接把像素除以 255 归一到 0 到 1 就够了因为像素的物理边界本来就是确定的。注意Min-Max 归一化的 max 和 min 必须来自训练集。如果用了全量数据的极值验证集和线上数据很容易落到 0 到 1 之外一些对输入范围敏感的模型会直接给出荒谬输出。3.2 偏态分布的修正对数、Box-Cox 与分箱真实数据里偏态是常态尤其是金额、时长、计数这类量几乎都是右偏长尾。线性模型和神经网络对偏态比较敏感因为少数极大值会主导梯度树模型对单调变换不敏感但对极端值的分裂点选择仍然会受影响。我常用的三种修正手段按使用频率排对数变换 log(1x)适合非负且右偏的数据那个加 1 是为了处理 0 值Box-Cox 或 Yeo-Johnson 变换适合需要自动寻找最优幂次的场景Yeo-Johnson 的好处是能处理负值分箱把一个连续变量切成若干区间变成有序类别适合那些数值本身不重要、落在哪个区间才重要的特征比如年龄分段、金额档位。分箱有个容易忽略的收益它能吸收掉一部分异常值的影响也能让线性模型表达非线性关系。代价是丢失了区间内的精细信息而且分箱边界如果按全量数据的分位数来定同样会造成泄漏必须按训练集分位数来切。3.3 类别特征编码独热、序数、目标编码各自的坑类别编码是预处理里选项最多、踩坑最密的一块。三种主流方式我这么用独热编码适合类别数少一般少于 15 个且无序的列比如性别、设备类型。它的坑在于类别数一多就维度爆炸一个城市列可能给你整出三百多维稀疏特征还顺手把树模型的分裂效率拖垮。序数编码适合本身有顺序的类别比如学历、评分等级。它的坑是把无序类别当有序用比如把红色1、蓝色2、绿色3喂给线性模型模型会真的以为绿色是红色的三倍。目标编码用类别对应标签的统计量替代类别本身适合高基数类别效果好但风险也最大必须用交叉验证或平滑的方式计算否则等于把标签直接写进特征里训练集指标漂亮得离谱验证集一塌糊涂。我通常还会加一个平滑项让样本数少的类别向全局均值靠拢避免个别稀有类别编码出极端值。4. 数据降维与特征选择维度不是越多越好4.1 PCA 的使用条件与常见误用PCA 做的事是找一组正交方向让数据投影后的方差最大。它有两个隐含前提数据应该是数值型且已标准化我们关心的是方差大的方向携带了更多信息。第一个误区是不标准化直接跑 PCA。假如你有两个特征一个是年收入万元级一个是年龄十到一百不标准化的话第一个主成分几乎就是年收入本身年龄的信息被完全淹没。所以跑 PCA 之前一定要标准化这一步不能省。第二个误区是拿 PCA 当万能的特征筛选器。PCA 的主成分是原始特征的线性组合可解释性差。做工业场景的模型时如果业务方要求告诉我哪个传感器最关键你给出一堆主成分是没法交代的。这种场景下我宁可做特征选择保留原始特征的物理含义。另外 PCA 是线性方法对非线性流形结构无能为力。如果数据分布明显是弯的用 t-SNE 或 UMAP 做可视化探索是可以的但别拿它们的结果直接当特征喂给下游模型因为这两个方法的邻域结构不稳定换个随机种子结果就变。4.2 特征选择三条路线怎么选特征选择我按计算成本从低到高分三档过滤式用统计指标打分比如方差、卡方、互信息、相关系数。它快、与模型无关适合第一轮粗筛把常数特征、近乎常数的特征、高度共线的特征先剔掉。缺点是只看单特征忽略特征组合效应。包裹式直接用模型效果做评价比如递归特征消除。它准但计算量随特征数指数增长特征上百列的时候基本跑不动。嵌入式把选择过程融进模型训练比如 L1 正则、树模型的特征重要性。这是我日常用得最多的一类成本可控结果也够用。用树模型的重要性做筛选时要注意一点重要性对高基数类别特征和取值多的连续特征有偏好城市这种几百个取值的列容易被误判为重要最好换成排列重要性permutation importance再确认一遍。4.3 降维之后还能解释吗工程上的折中工程里常见的折中是主线模型用原始特征加嵌入式选择保证可解释同时在探索阶段用降维快速看看数据结构、找找异常簇。降维结果用于诊断而不是直接上线这是我比较推荐的分工。还有一点降维和特征选择不是必须的环节。特征只有二三十列、样本量也够的时候硬上 PCA 反而可能损失信息。判断依据很简单如果去掉一部分特征后交叉验证指标没有下降甚至上升那就说明这些特征确实是噪声如果指标明显下降就别强行降维。5. 不同模态的数据预处理各有各的门道5.1 图片训练数据预处理从尺寸对齐到归一化与增强图片预处理我固定走这几步统一尺寸、统一色彩空间、归一化、增强。统一尺寸这一步如果是目标检测类任务直接缩放会改变目标的宽高比导致标注框失真这时候要用带 padding 的等比缩放。做医学图像分割比如 ISIC 2017 那类皮肤病变数据集图像里有大量黑色边框预处理时要先裁掉边框再缩放否则网络会浪费大量容量去学黑边这个毫无意义的模式。这是个很容易被忽略的点我看过不少复现效果不佳的案例问题就出在没裁边框。归一化按前面说的除以 255再加按通道的标准化用数据集自身的均值和标准差。这里的数据集统计量同样要在训练集上算别嫌麻烦。增强是图片任务里性价比最高的一步随机水平翻转、小角度旋转、随机裁剪、亮度对比度扰动。但增强必须符合语义比如数字识别不能水平翻转6 会变成不存在的东西医学影像的左右翻转也要慎重因为器官位置本身有左右语义。做照片修复、扩散模型这类生成任务时增强策略还要考虑退化模型的一致性训练时的退化方式必须和推理时的真实退化分布接近否则模型学到的映射用不上。5.2 文本与序列分词、标注与滑动窗口文本预处理的第一步是清洗去掉乱码、统一全半角、处理重复标点。中文还要分词分词工具的选择会影响下游效果尤其是做实体识别时分词边界和实体边界不一致会带来标注错位。实体识别类任务的预处理重点是标签对齐。原始标注通常是字符起止位置或者实体文本加出现次序要转成逐字符或逐词的 BIO 标签序列。这一步最容易出的错是文本被清洗后长度变了但标签还是按原文本算的导致标签整体错位模型学出来全是噪声。我一般的做法是清洗和标注对齐放在同一步完成任何会改变字符数量的操作都要同步更新标签位置。序列任务还有个绕不开的点是长度处理。做 TCN 这类时序卷积模型时感受野由卷积核大小和层数决定序列切分长度至少要覆盖你能接受的依赖范围做 Transformer 类模型时要注意位置编码和外推能力序列远长于训练长度时效果会掉。滑动窗口切分要注意窗口之间的重叠比例重叠太少会切断跨窗口的模式重叠太多又会让训练样本高度相关我一般取 20% 到 50% 之间具体看事件的持续时间长度。5.3 树模型与神经网络的预处理分歧同样一份数据喂给 XGBoost 和喂给神经网络预处理策略差别很大我这里列个对照预处理项树模型神经网络数值缩放基本不需要单调变换不影响分裂必须做否则梯度尺度失衡缺失值原生支持可以留缺失让模型自己学方向必须填充或加掩码类别特征可用序数编码或目标编码高基数也能扛一般用嵌入层独热维度太高偏态处理影响有限影响明显建议做变换特征共线性不敏感敏感建议剔除或正则化异常值相对稳健非常敏感建议截断这张表能解释不少为什么换个模型效果就崩了的疑惑。比如你把一份没做缩放的原始数据喂给树模型效果挺好转头换成神经网络就完全训不动问题不在网络结构在于你少做了标准化。6. 落地实操两条可复现的预处理流水线6.1 Pandas 版脚本从原始表到训练集这是我处理结构化数据时用的骨架可以直接抄。假设原始表里有设备编号、时间戳、压力、振动幅值、停机时长和标签。import numpy as np import pandas as pd df pd.read_csv(raw_device.csv, parse_dates[ts]) # 1. 去重同设备同时间只保留一条 df df.sort_values([device_id, ts]).drop_duplicates( subset[device_id, ts], keepfirst ) # 2. 物理规则修正 df.loc[df[stop_minutes] 0, stop_minutes] np.nan df.loc[df[pressure] 1500, pressure] np.nan # 超出量程 # 3. 缺失指示列 分组填充 df[pressure_missing] df[pressure].isna().astype(int) df[pressure] df.groupby(device_id)[pressure].transform( lambda s: s.ffill().fillna(s.median()) ) # 4. 分位数截断压住极端值 for col in [vibration, stop_minutes]: lo, hi df[col].quantile([0.01, 0.99]) df[col] df[col].clip(lo, hi) # 5. 对数变换修正右偏 df[vibration_log] np.log1p(df[vibration]) # 6. 时间特征拆解 df[hour] df[ts].dt.hour df[dow] df[ts].dt.dayofweek df[is_weekend] (df[dow] 5).astype(int)几个细节值得说。第 3 步用groupby加ffill是按设备分组做的避免跨设备串数据第 4 步的分位数来自当前数据集如果这是训练阶段没问题但如果是推理阶段应该把训练集算出的分位点存下来复用不能每次重新算第 5 步之所以单独生成一列而不覆盖原列是为了同时保留线性和非线性表达让模型自己选。6.2 sklearn Pipeline把预处理和模型绑在一起手工写预处理脚本最大的风险是训练和推理不一致训练时做了标准化推理时忘了或者填充用的均值不一样。用 Pipeline 能把这个问题从根上消掉。from sklearn.compose import ColumnTransformer from sklearn.pipeline import Pipeline from sklearn.impute import SimpleImputer from sklearn.preprocessing import StandardScaler, OneHotEncoder from sklearn.model_selection import train_test_split from xgboost import XGBClassifier num_cols [pressure, vibration_log, stop_minutes] cat_cols [device_type, shift] numeric_pipe Pipeline([ (imputer, SimpleImputer(strategymedian)), (scaler, StandardScaler()), ]) categorical_pipe Pipeline([ (imputer, SimpleImputer(strategymost_frequent)), (encoder, OneHotEncoder(handle_unknownignore)), ]) preprocess ColumnTransformer([ (num, numeric_pipe, num_cols), (cat, categorical_pipe, cat_cols), ]) clf Pipeline([ (prep, preprocess), (model, XGBClassifier(n_estimators400, max_depth6, learning_rate0.05)), ]) X_train, X_valid, y_train, y_valid train_test_split( X, y, test_size0.2, stratifyy, random_state42 ) clf.fit(X_train, y_train)这段代码的关键在于先划分再 fit。因为clf.fit只会用X_train去拟合那些 imputer 和 scalerX_valid只是被 transform。如果哪天需要保存模型上线直接把这个 Pipeline 整体 dump 出去推理时输入原始格式的数据即可中间所有统计量都跟着走不会出现线上线下不一致。handle_unknownignore也很重要它保证线上出现训练时没见过的类别时不会直接报错而是当成全零向量处理。我另外会做的一件事是把 Pipeline 的中间产物落盘检查一遍取几条验证数据把preprocess.transform的输出打印出来确认数值范围合理、没有全 NaN 的列、独热后的维度符合预期。这个检查花不了两分钟但能拦住大部分静默失败。7. 常见问题与排查技巧速查7.1 训练跑得好、上线就崩分布一致性怎么查线下指标高、线上拉胯我一般按这个顺序查先看特征分布有没有漂移再看预处理是否一致最后才怀疑模型本身。分布漂移的检查方法是把训练集和线上数据的每个特征画分位数对比表或者算 PSI群体稳定性指标。连续特征特别容易出问题比如线上某台设备的量程和训练集不同压力值整体高出一截标准化后虽然有缓解但如果超出训练集分布范围太多模型给出的预测本来就不可信。预处理一致性则要检查有没有哪个环节在线上被绕过。我遇到过的情况是训练脚本里有一步过滤掉停机时长超过 24 小时的记录但这个过滤规则没有写进线上服务导致线上来了一批超长停机样本模型根本没见过这种分布。凡是会影响样本集合的操作都要写进统一的预处理模块而不是散落在训练脚本里。7.2 症状对照表与排查顺序这张表是我这些年攒下来的出问题的时候按顺序对一遍能省不少时间症状最可能的原因排查动作训练 loss 一开始就是 NaN输入里有 NaN 或无穷值或学习率过大检查 transform 后是否有 NaN打印每列 min/max训练 loss 震荡剧烈输入尺度不统一检查是否漏了标准化看各列量级训练集指标高、验证集崩数据泄漏通常是缩放或目标编码用了全量数据检查 fit 是否只在训练集上训练集指标本身就低特征与标签无关或标签噪声大算单特征与标签的互信息人工抽查标签某类别样本预测全错该类在训练集中样本极少或被编码成了未知类别统计类别分布检查 handle_unknown 行为线上预测值恒定某一列线上全是缺失或常数打印线上 transform 后的方差换模型后效果骤降新模型对预处理有不同要求对照第 5.3 节的表格逐项检查7.3 我踩过的几个坑第一个坑是用全量数据算标准化参数。当时觉得数据都有为什么不全用上验证集指标确实漂亮上线后掉了七八个点。后来改成只在训练集 fit验证集指标从 0.92 掉到 0.88我心里还慌了一下但线上实测就是 0.88说明之前那个 0.92 是假的。第二个坑是类别编码引入了未来信息。做时间序列相关的分类任务时我用整个数据集的类别统计量做目标编码等于让模型看到了未来样本的标签分布线下指标虚高得离谱。改成分时间窗口滚动计算之后指标降下来了但也终于和线上对得上了。第三个坑是清洗时改变了文本长度却忘了对齐标签。做版面检测和实体识别时都遇到过这个问题模型训练 loss 正常下降但预测出来的框位置系统性偏移查了半天才发现是某个正则替换把连续空格合并了字符索引整体前移。后来我定了个规矩任何会改变字符数的文本清洗操作都必须放在标注对齐之前或者同步调整标注位置。第四个坑是图片预处理里把测试集的增强也打开了。训练集做随机翻转没问题测试集也开随机增强的话同一张图跑两次结果都不一样指标完全不可复现。正确的做法是测试阶段只做确定性的尺寸缩放和归一化把所有随机性关掉。第五个坑关于降维的时机。我曾经在一个二分类任务里先跑了 PCA 降到 20 维然后做特征选择效果一直不理想。后来把顺序调过来先做特征选择再从剩下的特征里做降维效果好了一截。原因是 PCA 会把噪声特征的方差也混进主成分里先降维等于让噪声参与了主成分构造。经验是如果特征有明显冗余先筛掉再降维如果特征之间高度相关且都需要保留那降维在前。这些坑的共同点是它们都不会报错只是安静地把效果拉低。所以我现在的习惯是每做完一份预处理都要回头抽查十几条样本从原始值一路看到最终喂给模型的向量确认每一步的变化都符合预期。这个动作看起来笨但它是我见过最有效的质量保障手段。另外一个小技巧是把预处理脚本的中间输出存成 parquet 缓存下次调模型时直接读缓存既省时间也保证每次实验用的是完全同一份数据——不然你会发现换了模型效果变好其实只是因为预处理脚本不小心改了一行。