资讯动态

数据预处理与特征工程实战指南:从清洗到编码的完整方法论

发布时间:2026/10/14 5:21:49 来源:尧图企业网站定制
开头写完了继续往下写主体。1. 数据预处理的核心思路与常见误区1.1 预处理的优先级为什么高于模型调参我先说一个判断数据预处理做得好不好直接决定模型效果的上限调参只是在逼近这个上限。这个道理很多人都听过但实际操作中一到项目里就变味了。我见过不少同学拿到一份数据先不管三七二十一跑个 baseline发现效果不行就调参调了两天提升不到 1 个点最后回头检查数据才发现时间字段解析错了、同一个用户 ID 在不同表里格式不一致、缺失值直接用 0 填了——这些问题任何模型都救不回来。原因很简单。模型学的是数据里的规律如果数据本身脏、乱、不一致模型学到的就是脏规律。你调参调的是“模型对规律的拟合方式”但规律本身是数据给的。数据预处理的核心优先级应该是先保证数据是可信的、一致性的、不含明显泄漏的然后再谈特征工程和模型选择。顺序反了后面全白干。1.2 先清洗还是先做特征顺序背后的逻辑很多人问我先清洗还是先做特征我的答案是先做一次快速清洗再做特征工程最后再回到清洗形成一个循环。为什么因为特征工程过程中会暴露很多数据问题。举个例子你在做时间特征时想把“注册时间”和“最后登录时间”相减得到活跃周期结果发现有一部分用户的最后登录时间早于注册时间——这显然不是用户穿越了而是数据录入或者同步出了问题。这种问题不做特征工程你根本发现不了。所以我现在的习惯是第一轮清洗只处理明显的错误和缺失比如空值、重复行、格式不一致做完一轮特征后对新生成的特征再做一次质量检查把遗留问题挖出来。两轮下来数据质量会扎实很多而不是指望一次清洗搞定一切。2. 清洗环节里真正见效的骚操作2.1 缺失值填充别只会均值和中位数缺失值处理是数据预处理里最基础也最容易糊弄过去的环节。最常见的就是 fillna(0)或者干脆填均值。说句实话某些场景下这么做确实没问题但更多时候会引入偏误。我举个例子用户收入字段存在缺失如果你直接填全体均值低收入和高收入群体的区分度会被拉低模型学出来就是一条歪斜的边界。我更推荐按分组统计填充。比如用户收入缺失可以按用户所在城市等级、职业类别分组后取中位数填充。这种做法的逻辑是同一个组的用户在收入分布上更相似用组内中位数比用全局均值靠谱得多。实现也不复杂一行代码就能搞定。import pandas as pd df[income] df.groupby([city_level, job_category])[income]\ .transform(lambda x: x.fillna(x.median()))如果缺失比例很高比如超过 30%我通常会新增一列income_missing_flag把缺失情况本身作为特征喂给模型。缺失这个事实本身就可能承载信息比如某些用户不愿意填收入这背后可能和人群属性有关。这招在风控和用户增长项目里经常有效。还有一个细节填充之后一定要检查填充值的分布和原始分布是否接近不要填充完就直接建模。你会发现很多时候填出来的中位数让整体分布出现了一个明显的尖峰这时候可以考虑用随机采样填充从一个合适的分布里采样填入来保持方差。2.2 异常值检测比 3σ 更稳的 MAD 法和 IQR 法很多教材里讲异常值检测就是 3σ 原则但实际数据里它一点都不稳。因为均值和标准差本身对异常值极度敏感一个极端大值把均值拉高、标准差拉大结果真实异常反而被淹没了。这就相当于让犯罪嫌疑人自己当法官逻辑上就不成立。我在实际项目中用得最多的是 IQR四分位距法和 MAD绝对中位差法。IQR 法大家相对熟悉一些小于 Q1-1.5*IQR 或者大于 Q31.5*IQR 的点都算异常。MAD 法对极端值更稳健它是用中位数替代均值、用绝对偏差的中位数替代标准差import numpy as np def mad_outlier_detect(s: pd.Series, threshold: float 3.0): median s.median() mad (s - median).abs().median() modified_z_score 0.6745 * (s - median) / mad return modified_z_score.abs() threshold这串代码里 0.6745 是把 MAD 转换为和标准差同尺度的常数这样 threshold 取 3 就和 3σ 口径近似但抗干扰能力强得多。处理异常值的方式也值得琢磨。我现在的习惯是训练集里的极端异常值如果是真实业务事件比如一笔合法的超大额交易不会直接删除而是用 Winsorize 截尾处理把超出上下限的值压到边界值如果确认是数据录入错误才会删除。删除之前一定要记录删了多少行、删的依据是什么这是数据审计的基本素养。2.3 脏数据里的格式问题字符串清洗的几板斧格式问题是数据预处理中最烦人的部分因为脏得毫无规律。身份证号里有空格、手机号带着86前缀、日期一会儿是2024/12/01一会儿是2024-12-01、还有那种把数字写成1,200的千分位格式。如果你不做统一模型会把它们当成完全不同的类别。我总结了一套字符串清洗的常规流程strip 去首尾空格统一大小写。用正则表达式把全角字符转半角。日期字段统一解析成标准datetime解析不了的记录单独标记出来。数值字段去掉千分位逗号、货币符号强制转成 float。电话、手机号这一类字段先去掉非数字字符再做长度校验。这些步骤看起来笨但能化解大量后端问题。特别是日期解析我踩过一次大坑一批数据里混了13月这种脏日期pandas 默认to_datetime解析会直接报错整个预处理流程中断。后来我在所有日期解析外面套了一个安全函数解析失败的日期统一置为 NaT 并填入缺失标志列流程再也没因为脏数据中断过。from datetime import datetime def safe_parse_date(val): if pd.isna(val): return pd.NaT try: return pd.to_datetime(val, errorsraise) except Exception: return pd.NaT3. 特征工程里的高效技巧3.1 时间特征拆出“周期性”比拆出“年月日”更有用时间特征是最常见也最容易被浪费的特征。很多人拿到时间戳就拆年、月、日、小时然后直接丢给模型。问题是模型并不知道“2 月”和“3 月”是相邻的对树模型来说它俩就是两个独立的类别完全没有顺序关系。你想表达时间的连续性这种拆法反而把信息打散了。我通常会额外构造三类时间特征周期位置、时间跨度、以及节假日标志。周期位置最常见的是把小时用三角函数编码让 23 点和 0 点在特征空间里是相邻的df[hour_sin] np.sin(2 * np.pi * df[hour] / 24) df[hour_cos] np.cos(2 * np.pi * df[hour] / 24)这个技巧在用户行为相关数据里很有用。比如外卖平台晚上 23 点和凌晨 0 点的行为模式其实高度相似但如果只按小时拆成类别特征模型会认为它们是两个毫无关系的离散值白白浪费了这种相似性。时间跨度特征是另一个重点。注册时间和当前时间的差值、上次登录距今天数、连续活跃天数——这些特征比单独放时间戳更能表达“用户活跃度”。注意计算跨度时单位要统一全部转成天或者小时不要一会儿秒一会儿天。3.2 数值特征的分桶与编码别让长尾数据干扰模型数值型特征经常面对长尾分布的困境比如用户消费金额少数大客户动辄消费几万大部分用户可能就几十几百。这种分布放进线性模型里会被极端值拉着走放进树模型里切分点也会集中在样本密度高的区域附近长尾信息被浪费。处理长尾的第一选择不是分桶而是取对数log1p能把消费金额压缩到相对均衡的区间。如果取完对数还是偏再考虑分桶按分位数把数据切成等频区间。这玩意儿可以自己写一个小函数方便重复调用def quantile_bucket(s: pd.Series, q: int 10) - pd.Series: try: buckets pd.qcut(s, qq, duplicatesdrop) except ValueError: buckets pd.cut(s, binsq) return buckets分桶之后最好保留原始值作为另一个特征因为分桶一定会损失部分精度二者互补信息更全。我在实际项目里发现对某些场景来说分桶后的类别特征和原始连续值特征同时喂给模型效果比单独用任何一个都要好因为树模型既能基于精确数值切分也能利用桶之间的粗粒度关系。3.3 类别特征编码标签编码、独热和频次编码的选择逻辑类别特征的处理经常引发争论。我的经验是高基数类别特征直接独热编码等于自杀比如城市有几百个独热后维度爆炸不说还容易过拟合。低基数类别比如性别、设备类型用独热没问题简单直接。对于高基数类别我强烈推荐频次编码和目标编码结合使用。频次编码用类别出现的次数替代类别本身逻辑是“出现频率高的类别往往对应主流群体”这个特征在很多场景下有不错的泛化能力。目标编码则是用该类别的目标均值替代类别本身但极其容易过拟合必须配合交叉验证来使用。# 目标编码示例需要放在交叉验证折内做防止泄漏 for fold_train_idx, fold_valid_idx in cv.split(X, y): train_fold, valid_fold X.iloc[fold_train_idx], X.iloc[fold_valid_idx] target_mean train_fold.groupby(category)[label].mean() valid_fold[category_target_enc] valid_fold[category].map(target_mean)说白了目标编码是拿“这个类别历史上大概是什么结果”作为先验告诉模型。顺手提醒一句这个操作特别容易造成数据泄漏小红书刷到很多人问为什么自己离线指标高、线上不涨我见过十有八九是目标编码做得太草率用了全量数据去算均值。3.4 特征组合从人工拼接过渡到多项式特征特征组合是提升模型表达能力的有效手段但“怎么组合”是有讲究的。最基础的做法是手工拼接比如把“城市”和“品类”拼成一个复合类别这在用户偏好挖掘里非常常见。复合之后原来每个维度看不太出来的规律就会浮现出来例如“北京夜宵”这个组合特征能显著区分某类用户行为而单纯看城市或者品类都没那么明显。如果特征都是数值型的可以直接使用 PolynomialFeatures 生成二阶交互项。用过的人都知道这个函数会产生一大堆组合需要注意控制数量否则维度膨胀严重影响效率。我更推荐的方式还是基于业务理解去人工选择交互对只构造那些在业务上有明确含义的组合特征剩下的交给模型自己去学。从实践来看特征组合的方向可以参考这几种思路用户特征 × 物品特征比如用户等级 × 价格段。时间特征 × 行为特征比如星期几 × 是否下单。地理位置特征 × 时间特征比如区域 × 时段。4. 提升预处理效率的工作流技巧4.1 pandas 的性能瓶颈与向量化改造很多人处理大一点的数据就喊卡根源在于 pandas 的 apply 循环太慢了。如果你在逐行用 for 循环遍历 DataFrame几百万行数据跑起来非常痛苦。我接手过一段别人写的预处理代码一个简单的规则函数用 for 循环跑了将近 40 分钟改成向量化操作后不到半分钟出结果。向量化的核心思路是能用内置方法就用内置方法能批量处理就绝不放循环。比如根据某列条件创建新列不要写循环直接用np.wheredf[is_high_value] np.where(df[amount] df[amount].quantile(0.9), 1, 0)如果业务逻辑确实复杂无法直接用内置函数可以先sort_values排序再用groupbyshift这类批量操作实现窗口计算而不是在纯 Python 里循环。实在需要逐行处理也优先用itertuples()而不是iterrows()前者比后者快好几倍。4.2 用 Pipeline 把预处理步骤固化成一个流程数据预处理最忌讳的是代码散落一地上一轮用的是这个写法下一轮又改成另一种写法最后连自己都搞不清处理逻辑是什么。我在项目中始终用 Pipeline 把预处理步骤串起来。这个做法还有一个额外的好处预测阶段你只需要调用同一个 pipeline 处理新数据不用重新写一遍清洗逻辑模型上线时也能保证线上线下逻辑一致。from sklearn.pipeline import Pipeline from sklearn.compose import ColumnTransformer from sklearn.impute import SimpleImputer from sklearn.preprocessing import OneHotEncoder preprocessor ColumnTransformer( transformers[ (num, SimpleImputer(strategymedian), numeric_features), (cat, OneHotEncoder(handle_unknownignore), categorical_features) ])这一步的价值在于保证每个环节可追溯上线排查的时候特别方便。你不需要记住昨天填缺失值用的中位数还是均值看 pipeline 配置一目了然。4.3 数据验证在预处理前和预处理后都要各检查一遍预处理流程扎实与否最后的检查很关键。我会在预处理开始前先跑一个数据快照记录行数、列数、缺失率、分布分位数、类别数量预处理结束后同样跑一遍快照。两份快照做对比能清晰看到每一步产生了什么影响异常情况一目了然。一个典型的检查策略包括行数是否与预期一致如果少了确认是去重还是过滤异常值导致的。数值字段的 min/max 是否在合理范围内类别字段的 unique 数量是否符合预期新生成的标签分布是否发生了偏移这套检查我吃过不少亏之后才养成的习惯。有一次做完预处理发现数据量少了 20%一查原来是 merge 的时候把外连接写成了内连接。如果没有快照对比这种错误可能到模型训练完才发现。5. 常见问题与排查技巧实录5.1 泄漏问题目标编码和特征选择里的隐形炸弹数据泄漏是预处理里最隐蔽、最致命的问题。最常见的一种是你在做特征工程的时候用了包含目标信息的全局统计量。比如做目标编码时用全量数据计算目标均值再映射回原数据这直接把验证集的信息灌进了训练集指标自然好看上线必翻车。正确的做法是一切涉及目标变量的统计计算必须在训练集内部完成并且通过交叉验证的方式在不同折内重复计算验证集和测试集只做映射不参与计算。一般而言泄漏问题很难用代码调试发现它需要你在设计特征时多问一句这个特征的计算过程是否使用了未来信息或样本外信息。另一种容易忽略的泄漏是特征选择环节。先用全量数据做特征筛选、再去做交叉验证这也是慢性泄漏的一种因为特征筛选过程已经“看过”全量数据的标签了。最好把特征筛选也放进交叉验证的每一折里面去做保证流程闭环。5.2 数据类型隐藏的坑object 列里全是数值读入数据后第一件事是查看 dtypes而不是直接开搞。pandas 里经常出现一种情况数值列因为混入了空字符串或者-被识别成 object 类型。你如果没发现后续所有计算都会报错或者自动变成字符串拼接结果完全不可用。我处理这种情况时用一个自定义函数先把列尝试转成数值无法转换的值记录为缺失再加一个标记列说明原值是什么形态。这样既保证类型正确又不丢失“这个单元格为什么不是数值”的信息。def safe_to_numeric(s: pd.Series): converted pd.to_numeric(s.astype(str).str.replace(,, ), errorscoerce) invalid_mask converted.isna() s.notna() return converted, invalid_mask.astype(int)5.3 缺失值填补后分布畸变的排查思路很多人在填充缺失值后发现模型效果反而变差了但找不到原因。一个大概率问题是填充造成的分布畸变。比如收入列用中位数填充后中位数那个位置会出现一个异常高的频数峰模型会误以为“中位数收入”是一个特殊群体反而学出一堆噪声规律。排查方法很直接画出填充前后该字段的分布直方图。如果填充后有明显的人造尖峰就说明填充策略引入偏误了。这种场景下我的建议是采用带噪声的随机填充先估计该字段的分布情况再从分布中抽样填充尽量保持原有方差。除了画图检查也可以对比填充前后的标准差、偏度系数一旦差异过大就要警惕。5.4 预处理常见问题速查表异常现象可能原因排查方向模型准确率正常但线上 A/B 不涨目标编码或特征选择泄漏检查统计特征是否用到全量数据数据量比预期少很多join 类型错误或过滤条件过严逐段对比快照检查每步行数变化收入等数值列全变成字符串拼接列被识别为 object检查原始数据是否混入脏字符填充后模型效果下降分布产生人造尖峰画分布直方图对比填充前后统计量日期解析总是报错中断存在异常日期格式统一用 safe_parse 捕获异常独热编码后维度爆炸高基数类别直接独热改用频次编码或目标编码结尾回头看我踩过的那些坑数据预处理大部分问题不是技术难度高而是不够细心、流程不规范。我现在做任何项目都坚持先跑数据快照再把清洗步骤沉淀成 pipeline每一步都留痕、可追溯。这个习惯看起来笨但能省下无数排查的时间。最后分享一个小技巧每次处理完一份数据顺手把特征说明表更新一下记下每个字段的含义、类型、取值范围、缺失比例和处理方式。这份文档平时不起眼但当你要复用数据、交接项目或者排查线上问题的时候它比任何代码注释都管用。预处理这件事慢就是快细就是稳。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑