4. 数据清洗基础:缺失值处理、异常值检测与重复数据去重
在推荐系统的特征流水线中,原始数据几乎不可能完美无缺。数据清洗是特征工程的第一步,也是决定模型效果下限的关键环节。本章聚焦三类最常见的数据质量问题:缺失值、异常值和重复数据,并给出在 PD(Personalized Recommendation)模型场景下的最佳实践。
4.1 缺失值处理
缺失值在用户行为日志、画像标签、上下文特征中普遍存在。处理策略分为三类:删除、填充和插值。
| 策略 | 适用场景 | PD 模型注意事项 |
|---|---|---|
| 删除 | 缺失比例 < 5% 且随机缺失 | 避免删除用户 ID 或 item ID 等关键主键 |
| 统计填充 | 数值型特征(均值/中位数/众数) | 均值填充会降低方差,影响树模型分裂 |
| 模型预测填充 | 特征间关联性强 | 用 KNN 或线性回归预测缺失值,计算成本高 |
| 插值法 | 时间序列特征(如用户近 7 天活跃度) | 线性插值、样条插值保持趋势 |
| 标记法 | 类别型特征或业务含义明确的缺失 | 将缺失作为独立类别(如 "unknown") |
PD 模型实战建议:对于用户实时特征(如“当前网络类型”),缺失往往代表“未获取到”,建议用标记法填充为 -1 或 "unknown",保留缺失的业务语义。
4.1.1 删除与填充代码示例
import pandas as pd
import numpy as np
# 示例数据:用户特征表
df = pd.DataFrame({
'user_id': [1, 2, 3, 4, 5],
'age': [25, np.nan, 30, 22, np.nan],
'gender': ['M', 'F', np.nan, 'F', 'M'],
'last_7d_clicks': [12, 5, np.nan, 8, 15]
})
# 1. 删除缺失比例超过 50% 的列
df.dropna(thresh=len(df) * 0.5, axis=1, inplace=True)
# 2. 数值型填充:用中位数填充年龄
df['age'].fillna(df['age'].median(), inplace=True)
# 3. 类别型填充:用众数填充性别
df['gender'].fillna(df['gender'].mode()[0], inplace=True)
# 4. 时间序列插值:线性插值填充 last_7d_clicks
df['last_7d_clicks'] = df['last_7d_clicks'].interpolate(method='linear')
print(df)
4.2 异常值检测与处理
异常值在推荐特征中常表现为:用户单日点击量异常高、商品价格录入错误、连续特征出现极端离群点。异常值会严重干扰特征缩放和模型训练。
4.2.1 常用检测方法对比
| 方法 | 原理 | 适用特征类型 | PD 场景示例 |
|---|---|---|---|
| Z-Score | |Z| > 3 视为异常 | 近似正态分布 | 用户年龄、商品价格 |
| IQR 四分位距 | Q1 - 1.5*IQR 或 Q3 + 1.5*IQR 之外 | 偏态分布 | 用户点击次数、停留时长 |
| 孤立森林 | 基于树模型隔离异常点 | 高维特征 | 多维度用户画像异常检测 |
| DBSCAN | 密度聚类,低密度点为异常 | 空间/行为序列 | 用户行为轨迹异常 |
4.2.2 异常值处理策略
- 截断(Winsorize):将超出阈值的值替换为阈值边界值,保留样本数量。
- 删除:当异常值明显为录入错误且数量极少时直接删除。
- 视为缺失:将异常值标记为缺失,再走缺失值处理流程。
- 分箱离散化:将连续值分箱,异常值落入首尾箱,降低影响。
避坑提醒:不要对类别型特征(如 gender、city)做 Z-Score 异常检测。类别型特征的异常应通过频次过滤处理,例如出现次数 < 10 的类别合并为 "other"。
4.2.3 IQR 异常检测代码示例
def detect_outliers_iqr(series):
Q1 = series.quantile(0.25)
Q3 = series.quantile(0.75)
IQR = Q3 - Q1
lower_bound = Q1 - 1.5 * IQR
upper_bound = Q3 + 1.5 * IQR
return (series < lower_bound) | (series > upper_bound)
# 示例:检测用户点击量异常
df['click_outlier'] = detect_outliers_iqr(df['last_7d_clicks'])
# 截断处理
click_col = 'last_7d_clicks'
lower = df[click_col].quantile(0.05)
upper = df[click_col].quantile(0.95)
df[click_col] = df[click_col].clip(lower, upper)
4.3 重复数据去重
重复数据在推荐系统中主要来自:数据采集重试、多源数据合并、日志重复上报。重复数据会导致模型对某些样本过拟合,并扭曲统计特征。
4.3.1 去重策略
- 完全去重:所有列完全相同的行只保留一条。
- 基于主键去重:按 user_id + item_id + timestamp 等关键字段去重,保留最新或最早记录。
- 模糊去重:对文本特征(如商品标题)用编辑距离或 SimHash 去重,保留质量最高的一条。
4.3.2 代码示例
# 完全去重
df.drop_duplicates(inplace=True)
# 基于主键去重:保留最后一条记录
df.drop_duplicates(subset=['user_id', 'item_id', 'date'], keep='last', inplace=True)
# 查看重复统计
print(f"去重前样本数: {len(df)}")
print(f"重复行数: {df.duplicated().sum()}")
print(f"去重后样本数: {len(df.drop_duplicates())}")
PD 模型实战要点:在构建训练样本时,务必先做重复数据去重,再做缺失值和异常值处理。否则重复数据可能掩盖真实的缺失比例和异常分布。
4.4 数据清洗在 PD 模型流水线中的位置
数据清洗不是一次性工作,而是特征流水线中的前置标准化步骤。在 PD 模型的生产环境中,清洗逻辑通常封装在特征计算层,确保离线训练和在线推理使用相同的清洗规则。
| 流水线阶段 | 清洗动作 | 输出 |
|---|---|---|
| 原始日志接入 | 去重、过滤无效记录 | 干净的事件流 |
| 特征计算 | 缺失值填充、异常值截断 | 稳定的特征值 |
| 特征存储 | 统一缺失标记、类型校验 | 可回溯的特征快照 |
| 训练样本构建 | 最终清洗、样本去重 | 高质量训练集 |
最佳实践:将清洗规则以配置文件(YAML/JSON)形式管理,版本化存储。每次清洗规则变更后,重新回溯历史特征,保证训练和推理数据分布一致。