1.1 "脏数据"不是只有缺失值
我把这些年见过的高频脏数据问题整理成了一张表,遇到数据文件时直接对照着查:
| 问题类型 | 具体表现 | 常见来源 | 处理优先级 |
|---|---|---|---|
| 缺失值 | 某个字段大量为空或部分为空 | 用户不填、系统采集中断、多表关联丢失 | 高 |
| 异常值 | 数值明显超出业务合理区间 | 传感器故障、手工录入错误、极端真实事件 | 高 |
| 重复值 | 全行重复或关键字段重复 | 多次导入、数据源重复推送 | 高 |
| 格式混乱 | 日期格式不统一、编码不一致 | 多系统导出、跨地区数据源 | 高 |
| 量纲差异 | 销售额几万、折扣率零点几 | 原始业务字段直接拼接 | 中 |
| 语义不一致 | 性别写成男/M/1/男性 | 不同系统字典不同 | 中 |
| 无效字段 | 某一列几乎全是同一个值 | 常量列、未更新字段 | 低 |
大部分新人只盯着第一类"缺失值"处理,却忽略了其他更隐蔽的问题。我印象最深的一个项目里,某门店ID列在订单表里是字符串"1001",在门店表里是数字1001,合并时没有统一判断,直接导致关联后丢了两千多条记录,这种问题不仔细看根本发现不了。
1.2 数据预处理的边界:到什么程度算处理完
处理到什么时候可以停手?这是团队里经常争论的问题。我的判断标准是三个状态:
第一是合法,字段类型全部正确、值域在合理范围、类别取值统一。第二是一致,多表能够顺畅对齐,主键没有冲突。第三是可用,数据分布满足建模算法的最基本假设,类别变量已经编码,数值变量的量纲正常。
我习惯在开工前先跑一遍质量检查清单:文件读取是否报错?列名和 dtype 是否符合预期?还有没有空值?每个分类字段的取值集合是什么?数值字段的取值范围是否有意外?有没有全列都一样的常量列?有没有重复行?这七个问题全部过一遍,再决定下一步做什么。
做预处理最忌讳的就是埋头写代码,写了几百行清洗逻辑才发现源数据结构理解错了,返工成本极高。
2. 四步主线:清洗、集成、变换、规约,以及怎么画项目流程图
数据预处理有很多种框架,业界最常用的是R:《数据挖掘:概念与技术》中的四阶段模型,把它当成主线来规划项目结构,个人和团队协作都方便。
code复制原始数据 → 数据清洗 → 数据集成 → 数据变换 → 数据规约 → 建模就绪数据
但要强调一点,实际项目里这条链路绝对不是线性的。我做过一个零售分析项目,清洗完订单表之后发现门店表和订单表的关联字段格式对不上,只好退回集成阶段重新统一。所以真实的流程图应该是一个带反馈环的循环,每个阶段都可能会跳回前序阶段。画流程图时,我建议把"数据探查"放在最前面,因为清洗、集成、变换的方案全部依赖探查结果。
2.1 数据清洗是底线工程
清洗解决的是数据的"脏、缺、重、乱"四个问题。脏指字段取值不合理,缺指缺失值,重指重复记录,乱指格式不统一。在操作顺序上,我习惯先做格式统一,再做缺失值处理,最后处理异常值和重复值。理由很简单:有些格式问题(比如"TRUE"和"True")不统一,去重时会误判成不同的值;日期格式不统一时,按时间排序去重也会出错。
清洗环节最需要业务知识的介入。你不可能脱离业务单纯靠统计方法判断某个值是缺失、是异常、还是真实存在的极端情况,这一点在后面的异常值处理部分会详细展开。
2.2 数据集成:多表合并不是拼个 concat
常见误区是把"集成"等同于 pd.concat 或者 SQL 里把表拼在一起。实际要处理的问题多得多。
首先是主键字段名不一致,两张表里一个叫 ShopID 一个叫 shop_id。其次是字段编码不一致,同一家门店在一张表里叫 1001,在另一张表里叫 A1001。更复杂的是关系没有理清楚,连表之前没确认是一对一、一对多还是多对多,直接 left join 之后行数爆炸。
我的建议是正式合并前,先单独检查每张表主键的唯一性。再用小样本数据分别做一次 inner join 和 outer join,对比行数差异,能非常有效地暴露关联逻辑的问题。
2.3 数据变换:让不同量纲的数据"说同一种语言"
数据变换解决的是"算法能不能直接用"的问题,基础操作包括三个方向:
连续变量离散化。比如把年龄切成青年、中年、老年,或者把销售额按分位数分成高、中、低档。这个操作可以帮助模型捕捉非线性关系,但也容易丢失信息,要谨慎。
数值变换改变分布形态。比如对长尾数据做对数变换,这是处理偏态分布最常用的手段之一。
标准化与归一化。让不同量纲的数值特征处在一个可比的范围,具体方法后面单独讲。
还有一个必须放在这里的操作是类别变量编码。很多算法只能吃数值,吃不了"红""绿""蓝"这种字符串,得把类别转成数字。编码方式的选择直接影响特征维度、模型解释性和最终效果,这里面的学问也很大。
2.4 数据规约:不是非要用 PCA
数据规约的目的是在尽量保留信息的前提下减少数据规模,从而提升计算效率。方法大致分三类:维度规约(PCA、特征选择)、数量规约(抽样、重采样)、数值规约(用更少的状态表示数据)。
我最想提醒的是,别把 PCA 当标准动作。很多项目的特征本来就几十个,样本量也不大,训练时间根本不是瓶颈,强行降维唯一的作用是让模型损失一部分信息的可解释性。规约应该是在数据量大、计算慢、存储压力大的场景下才动用的一项可选操作。
数据规约的另一个实用手段是采样。类别极度不平衡时,下采样多数类或上采样少数类都是常规操作,这些也属于规约的范畴。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
3. 用 pandas 写一套可直接复用的清洗代码
前两部分讲概念,这部分直接进代码。以下代码基于 pandas 和 numpy,是我最常用的基础清洗模板,可以直接复制到项目里改字段名使用。
3.1 第一步永远是探查,不是清洗
python复制import pandas as pd
import numpy as np
# 读取数据,编码问题最常见
df = pd.read_csv('raw_data.csv', encoding='utf-8')
# 如果上面报错,试试 gbk/gb2312 编码
# df = pd.read_csv('raw_data.csv', encoding='gbk')
# 结构化探查
print('数据形状:', df.shape)
print('\n列名和类型:')
df.info()
print('\n缺失值统计:')
print(df.isnull().sum())
print('\n重复行数:', df.duplicated().sum())
print('\n唯一值数量:')
print(df.nunique())
这一串代码我每次都会跑,跑完数据的基本盘就有数了。有一点要注意:df.isnull().sum() 只能反映空值和 NaN,如果填的是字符串 "null"、"NULL"、""、空格这类值,统计不到,需要额外检查。
python复制# 检查被填成字符串的空值
for col in df.columns:
if df[col].dtype == 'object':
null_like = df[col].isin(['null', 'NULL', 'None', '', ' ', 'nan', 'NaN']).sum()
if null_like > 0:
print(f'{col} 列有 {null_like} 个类空字符串')
3.2 缺失值处理的三种策略和适用场景
缺失值处理不是选哪个方法的问题,而是基于"为什么缺失"来做判断。一般来说就三条路:删除、填充、插值。
直接删除适用于两种情况:一是缺失的样本占总样本的比例很小,比如 1% 以内,删掉不影响整体分布;二是缺失字段就是你的目标变量,比如预测销量但销量本身就缺失的行,没法填充,只能删除。
python复制# 删除目标变量缺失的行
df = df.dropna(subset=['sales_qty'])
统计量填充适用于字段本身分布稳定、缺失原因和业务无关的情况。数值字段常用中位数填充(比均值更抗异常值,我用中位数比均值多),分类字段找众数填充。
python复制# 数值列用中位数填充
df['temperature'] = df['temperature'].fillna(df['temperature'].median())
# 分类列用众数填充
df['weather_type'] = df['weather_type'].fillna(df['weather_type'].mode()[0])
# 按组填充,比如每个门店分别用自己历史的平均温度填充
df['temperature'] = df.groupby('store_id')['temperature'].transform(
lambda x: x.fillna(x.median())
)
插值法更适合时间序列数据。对股票、传感器这类连续时序信号,缺失值不能用全局均值或者中位数填,因为时序数据的局部特征比全局水平重要得多。优先用前向填充、后向填充或者线性插值。
python复制# 时序数据填充示例(已按时间排序)
df['value'] = df['value'].ffill() # 用上一个值填充
df['value'] = df['value'].bfill() # 剩下的开头缺失用下一个值
# 更平滑的选择:线性插值
df['value'] = df['value'].interpolate(method='linear')
3.3 异常值用 3σ 还是 IQR?代码和边界
异常值判断最常用的是两种统计方法。
3σ 法基于正态假设:数据在均值 ± 3 个标准差以外的概率很低,视为异常。但真实业务数据大多不是正态分布,直接套 3σ 会把正常的业务波动标记成异常,所以要提前做分布检验,比如用 histogram 或者 Shapiro-Wilk 先看一眼。
python复制# 3σ 方法检测异常值
z_scores = np.abs((df['sales_qty'] - df['sales_qty'].mean()) / df['sales_qty'].std())
outlier_mask = z_scores > 3
print(f'3σ 方法识别出 {outlier_mask.sum()} 个异常值')
IQR 方法是基于四分位距的,不受极端值影响,不需要正态假设,我日常工作用 IQR 更多。
python复制# IQR 方法检测异常值
Q1 = df['sales_qty'].quantile(0.25)
Q3 = df['sales_qty'].quantile(0.75)
IQR = Q3 - Q1
lower_bound = Q1 - 1.5 * IQR
upper_bound = Q3 + 1.5 * IQR
outlier_mask = (df['sales_qty'] < lower_bound) | (df['sales_qty'] > upper_bound)
print(f'IQR 方法识别出 {outlier_mask.sum()} 个异常值')
# 查看异常值具体信息
df[outlier_mask][['store_id', 'sales_qty', 'promotion_flag']]
但不管是 3σ 还是 IQR,都只是"发现可疑值"的工具,不等于"可以删除"。波动本身可能就是业务真相。
比如大促期间的销量翻好几倍,对销售预测模型来说那是宝贵信号,不应该删。再比如交易欺诈检测场景里,绝大部分欺诈样本都长在异常值里,删掉异常值等于删掉训练目标。
我的习惯是:先跑统计方法圈出候选异常样本,再逐条看业务上下文,能抠出明确错误原因且有业务方确认的,才去删除或修正,其他情况宁可保留或者在模型里单独处理。
python复制# 不删除而是封顶/压底(winsorize)
df['sales_qty_clipped'] = df['sales_qty'].clip(lower=lower_bound, upper=upper_bound)
3.4 重复值、列名、类型统一的细节
重复值分成两种,一种是整行一模一样,这种一般直接删,另一种是某几个关键字段相同,比如同一天同一门店同一条促销记录出现了两次,这个需要结合业务判断。
python复制# 整行完全重复检查
print(df.duplicated().sum())
df = df.drop_duplicates()
# 按关键字段去重,比如日期+门店ID+商品ID 维度应当唯一
df = df.drop_duplicates(subset=['date', 'store_id', 'product_id'])
处理重复之前一定要先确认业务主键是什么。我曾经见过一个项目,同事没确认主键就把重复行删了,结果把一单多件商品的记录当成重复数据清掉,损失很大。实操项目里先跑一句 df.value_counts 看看同一主键下是否真的对应多条不同记录,比直接 drop_duplicates 安全得多。
列名和类型统一也是清洗的一部分,常在数据集成前做。
python复制# 列名统一
df.columns = [col.strip().lower().replace(' ', '_') for col in df.columns]
# 类型转换
df['date'] = pd.to_datetime(df['date'])
df['store_id'] = df['store_id'].astype(str)
df['promotion_flag'] = df['promotion_flag'].astype(bool)
df['price'] = pd.to_numeric(df['price'], errors='coerce') # 转不了变成NaN
日期字符串格式不同是最常见的类型问题,Excel 导出可能会混着 "2024-01-01" 和 "2024/1/1" 这两种格式,pandas 的 to_datetime 通常能一并解析,但极个别情况下需要手动指定 format。
4. 变换与编码:标准化、归一化、分箱、one-hot 的完整操作方法
清洗完的数据仍然不等于模型能吃的数据。这个阶段处理的核心是"算法友好性",要让特征在格式、量纲、分布上尽量匹配模型的偏好。
4.1 StandardScaler 与 MinMaxScaler 的选择逻辑
数值缩放最常用的有两种:StandardScaler 和 MinMaxScaler,分别对应 z-score 标准化和区间缩放。
StandardScaler 把数据变成均值 0、标准差 1,适合数据近似正态分布的情况,以及线性回归、逻辑回归、SVM、神经网络这类依赖距离计算的模型。MinMaxScaler 把数据缩放到 [0, 1],适合数据本身有明确上下界、分布不要求正态、特征是稀疏矩阵的情况。
我个人的选择逻辑是这样:
| 场景 | 推荐方法 | 原因 |
|---|---|---|
| 特征近似正态分布 | StandardScaler | 保留分布形状,利于回归模型 |
| 分布严重偏态 | 先 log 变换再标准化 | 直接缩放效果差 |
| 数据有明确边界(如百分比) | MinMaxScaler | 保持区间语义 |
| 稀疏数据、图像像素 | MinMaxScaler | 避免破坏稀疏性 |
| 树模型 | 可不用缩放 | 树模型对量纲不敏感 |
代码:
python复制from sklearn.preprocessing import StandardScaler, MinMaxScaler
scaler_std = StandardScaler()
df['temp_std'] = scaler_std.fit_transform(df[['temperature']])
scaler_minmax = MinMaxScaler()
df['sales_minmax'] = scaler_minmax.fit_transform(df[['sales_qty']])
重要提醒写在后面:Scaler 必须在训练集上 fit,再拿同一个已经 fit 好的 Scaler 去 transform 测试集,绝对不能对全量数据 fit_transform 之后再做训练测试拆分,否则会造成信息泄漏。
4.2 处理倾斜分布的 log 变换
很多真实业务特征都是右偏的,比如销量、点击量、用户收入。此时直接 Z-score 或者 MinMax 只是把值压平了,分布形状并没有改变。log 变换能把长尾拉回接近正态,让模型尤其线性模型学得更稳。
我惯用的代码:
python复制import numpy as np
# 对销售金额做 log1p 变换(加了1避免0取对数问题)
df['sales_amount_log'] = np.log1p(df['sales_amount'])
# 模型预测之后要还原
df['sales_amount_predict_exp'] = np.expm1(predicted_array)
这里最容易踩的坑是只变换训练集特征,忘记在模型输出层把预测结果反向还原,算评估指标时对不上量级才知道出事了。另一个容易出问题的地方是变换之后要检查一下新列是否还保留原来的业务语义,log 后的销售金额不再是"元",而是"元的对数",直接看系数解释时别搞混。
还有 Box-Cox 和 Yeo-Johnson 变换,前者要求数据为正,后者不要求,sklearn 里都有现成实现,效果往往比 log 更专业,但可解释性差一些,我用得比较少。
4.3 分类变量编码:one-hot、label encoding 与 target encoding 怎么挑
类别编码最常遇到的方案是三个,简单说下我的选择标准:
如果类别数量少(比如天气类型只有晴、雨、阴),可以用 one-hot。优点是直观,没有隐含顺序信息。
python复制# 方法一:pandas 自带 one-hot
weather_dummies = pd.get_dummies(df['weather_type'], prefix='weather')
df = pd.concat([df, weather_dummies], axis=1)
# 方法二:sklearn 的 OneHotEncoder(更适合集成到 pipeline)
from sklearn.preprocessing import OneHotEncoder
encoder = OneHotEncoder(sparse_output=False, handle_unknown='ignore')
encoded = encoder.fit_transform(df[['weather_type']])
如果类别之间存在明显的等级关系,比如学历、评分等级,用有序编码更合理。
python复制grade_map = {'小学': 0, '初中': 1, '高中': 2, '本科': 3, '硕士': 4, '博士': 5}
df['education_level'] = df['education'].map(grade_map)
如果类别非常多(比如几百个城市、几千个门店),盲目 one-hot 会得到又宽又稀疏的特征矩阵,树模型性能也会受影响。这时候常用两类做法:一类是频次编码,用类别出现的次数作为特征;另一类是 target encoding,用类别对应的目标均值作为特征。target encoding 信息量更大但过拟合风险也大,一定只能在训练集里计算编码值,并且做完要配合交叉验证评估。
另外提醒一个比较隐蔽的坑:one-hot 之后用线性模型会引发多重共线性,也就是常说的"哑变量陷阱"。解决办法是丢弃一个哑变量,pd.get_dummies(..., drop_first=True) 就可以。
5. 一个销售预测项目的预处理全流程复盘
讲了这么多理论,用一个实际项目把完整流程串起来。背景是某连锁零售企业要做下个月日销量的预测,数据源来自三张表:订单明细表、门店维表、天气记录表。目标是预测每个门店每天的销量。
5.1 项目背景和原始数据长什么样
三张表的原始情况:
订单明细表:包含订单号、日期、门店ID、商品ID、销量、销售金额、是否促销(字段值有 True/False/1/0/是/否六种情况)。
门店维表:包含门店ID(数字类型)、门店名称、门店地址、所在城市、面积。
天气记录表:按城市记录了日期、天气类型(晴/雨/阴,有缺失)、温度(有缺失)、降水量。
拿到数据之后我先做的是数据探查,不急着写任何清洗逻辑,把每张表的信息看了一遍。订单表有约 62 万行,日期跨度为 2023-01-01 至 2024-07-31;门店表有 120 家门店;天气表按城市粒度记录,但天气字段缺失率约 18%,温度缺失约 8%。
5.2 分步骤处理逻辑
第一步统一编码和类型。
订单表的促销字段六种写法统一成布尔值;订单表的门店ID 转字符串,和门店表对齐;日期统一成 datetime64 类型。
python复制# 促销标记统一
df_order['promotion_flag'] = df_order['promotion_flag'].astype(str)
df_order['promotion_flag'] = df_order['promotion_flag'].map({
'True': 1, '1': 1, '是': 1, 'true': 1,
'False': 0, '0': 0, '否': 0,
})
第二步处理缺失。
天气表的天气类型是分类变量,直接用众数填充;温度数值型,但不同城市、不同季节的温度差异很大,全局均值填充会抹掉地域差异,所以我按城市分组取中位数填充。这比简单的 df.fillna(df['temperature'].mean()) 靠谱得多。
python复制df_weather['temperature'] = df_weather.groupby('city')['temperature'].transform(
lambda x: x.fillna(x.median())
)
订单表和门店表按门店ID合并后,没有出现明显的新增缺失,订单的目标字段销量没有缺失,不需要动。
第三步做异常值识别。
我对销量字段第一个动作是画箱线图,发现右侧有一堆离群点。先不急着判定为异常,把离群样本拉出来看了下日期,几乎全部是双十一、618 大促当天的记录。
这些是真实的业务高峰,并不异常。但如果不处理,模型拟合普通日子时会被这几个高峰拉偏。最终我的处理是保留促销日的样本,但在特征中增加一个"是否大促日"的标记列,让模型自己学习不同日期状态下销量的差异。针对明显异常的销量为负、售价为 0 的记录,再做剔除。
python复制# 剔除明显不合理的记录
df_order = df_order[(df_order['sales_qty'] > 0)]
df_order = df_order[(df_order['sales_amount'] > 0)]
第四步构造特征。
日期拆出年、月、日、星期几、是否周末。是否促销保留。天气类型做 one-hot 编码。又构造了一个"该门店最近 7 天平均销量"的滚动特征。
python复制df_sorted = df_order.sort_values(['store_id', 'date'])
df_sorted['sales_7d_mean'] = df_sorted.groupby('store_id')['sales_qty'].transform(
lambda x: x.shift(1).rolling(7, min_periods=1).mean()
)
第五步做数值特征的标准化。
我最终用了 StandardScaler,因为后面用的是 XGBoost 和线性回归组合模型,线性回归部分对量纲敏感。注意 Scaler 只 fit 训练集。
python复制from sklearn.preprocessing import StandardScaler
scaler = StandardScaler()
x_train_scaled = scaler.fit_transform(x_train[numeric_cols])
x_test_scaled = scaler.transform(x_test[numeric_cols])
5.3 时序数据划分的坑
销售预测是时序数据,训练集和测试集不能随机切分,必须按时间戳严格排序。
我一开始按平时的习惯用了 train_test_split(random_state=42),结果验证集得分虚高到不可思议,后来才发现因为用户行为有周期性,随机切分让训练集和验证集都包含了同一个时间段的样本,模型相当于"提前背了答案"。
正确做法是取前 80% 的时间段做训练,后 20% 做验证。具体到这个项目,按日期排序后:
- 训练集:2023-01-01 至 2024-03-31
- 验证集:2024-04-01 至 2024-07-31
python复制train = df_sorted[df_sorted['date'] < '2024-04-01']
test = df_sorted[df_sorted['date'] >= '2024-04-01']
时序切分之后还要注意一个问题。如果训练期和验证期的分布差异特别大,比如一个在常规销售期,一个在大促期,模型就会在验证集上表现异常。这种时候要单独讨论,是多留一些包含大促的历史数据进训练,还是把大促日单独建模,没有统一答案,取决于业务对峰值预测的重视程度。
6. 踩坑实录:数据预处理的常见错误(含自查清单)
作为收尾,我把这几年高频踩到的预处理坑集中列出来,这些都是会让模型结果失真、甚至整个项目推倒重来的大坑,建议对照排查。
6.1 信息泄漏:transform 和 fit 用错
这是最常见的坑,也是最严重的坑。表现是用 StandardScaler 对全量数据 fit_transform,然后再拆分训练集和测试集。测试集的信息在 fit 的时候已经透传给了训练集,验证结果会虚高,模型上线后效果立刻崩掉。
正确姿势是:
python复制# 错误示范
scaler = StandardScaler()
x_all_scaled = scaler.fit_transform(x_all)
X_train, X_test, y_train, y_test = train_test_split(x_all_scaled, y_all)
# 正确示范
X_train, X_test, y_train, y_test = train_test_split(x_all, y_all)
scaler = StandardScaler()
X_train_scaled = scaler.fit_transform(X_train)
X_test_scaled = scaler.transform(X_test)
同样的逻辑也适用于缺失值填充中的均值/中位数计算、target encoding 的编码值计算。凡是涉及全量数据统计量的操作,全部要在训练集上算完之后再映射到测试集。sklearn 的 Pipeline 能很好解决这个问题,推荐优先使用。
6.2 全局均值的陷阱
缺失值填充看起来很简单,填错了却很致命。最典型的是用全量数据的均值或者中位数去填所有缺失值,却忽略了数据本身的分组结构。
我给过一个实际例子:温度按城市分组填充,比全局填充合理得多,因为北京和广州的温度均值差距极大,用全局均值填会把地域差异抹平,让后续模型学不到"城市对销量有影响"这个重要关系。
所以在填充缺失值之前,先问自己三个问题:这个字段在不同分组下的分布差异大吗?缺失是随机缺失还是有某种偏好?填充值是否会造成数据分布的人为偏移?按分组填充后记得对比填充前后的分布直方图,确认没把分布搞出明显畸变。
6.3 哑变量陷阱和类别过多
one-hot 编码用得很顺手,但对高基数类别字段无脑 one-hot,会带来两个问题。
线性模型下的多重共线性。三个互斥类别用三个 0/1 列表示,这三个列必然线性相关,因为一列可以完全由另外两列表示。这不影响树模型,但会让线性回归或逻辑回归的参数估计不稳定。pd.get_dummies(df, drop_first=True) 可以解决。
稀疏维度爆炸。当类别数有几百个时,one-hot 之后特征数量暴增,样本却很稀疏,计算变慢不说,可能还拉低模型效果。应对方案是先把出现次数少的类别合并为"other",或者改用一个低频类别跳过的编码方案,再实践的话用 target encoding 效果更直接。
我之前处理过城市字段,几百个城市直接用 one-hot,模型训练时间暴涨三倍,效果还退步了。把出现次数低于 100 次的城市合并成"other"之后,模型性能才回到正常水平。
6.4 自查清单
项目交付前我习惯逐项过一遍这张表,每次都多多少少能翻出一些问题:
| 检查内容 | 检查结果 |
|---|---|
| 文件读取正常,编码无报错 | |
| 列名规范统一,无空格和大小写混淆 | |
| 字段类型正确:日期是 datetime、类别是字符串、数值是数字 | |
| 缺失值处理完毕,且能说明为什么用这个方法 | |
| 异常值有业务判断记录,没有机械删除 | |
| 重复值已按业务主键识别并处理 | |
| 分类变量编码已确认,未出现哑变量陷阱 | |
| 标准化/归一化只在训练集上 fit | |
| 目标编码/统计量填充没有信息泄漏 | |
| 时序数据没有随机打乱切分 | |
| 数据预处理代码和配置有版本记录 |
关于最后一项版本记录,多说一句。我在早期做项目时吃过亏,模型一个月后复跑,结果对不上,追查根源是有人改了预处理脚本里一个填充参数,但没有任何记录。后来强制要求把预处理使用的所有参数、缺失值处理方式、列合并/删除记录都写在一个 config.py 或 Markdown 文件里,项目复盘和复现的效率大幅提升。
数据预处理这套工程方法,核心不是代码写得有多花哨,而是每一步都清楚"为什么这么做"。业务场景不一样,同一列数据的处理方式可能完全相反。建议读者在做完一个预处理流程后,顺手把每个决策的理由记录下来,下次遇到类似场景直接翻出来参考,比到处找网上的零散代码要高效得多。
