第3章:数据收集与清洗复盘——从混乱到可用
数据收集与清洗,听起来像是脏活累活,对吧?说实话,在MD特征学习项目里,这一步往往是决定整个项目成败的关键。模型选得再好,架构搭得再漂亮,喂进去的数据是垃圾,那出来的结果也只能是垃圾。
我早期带项目时,最怕的就是复盘时发现数据源头就有问题。那种感觉,就像打了一场大仗,最后发现枪里装的是空包弹。所以这次复盘,咱们得把数据这关把死。
3.1 数据收集复盘:不只是“拉数据”那么简单
很多团队复盘时,只关心数据量够不够,样本分布均不均匀。但我个人的复盘习惯,会多问几个“为什么”。
为什么特定样本数量少?是采集策略有问题,还是原始来源本身就有偏向?我见过一个项目,模型在测试集上表现极好,结果上线就崩。复盘发现,训练数据里超过80%是白天场景的样本,晚上样本几乎为零。你说这模型能不出事吗?
数据来源这块,我建议你用下表来做框架式复盘。这不是死规矩,是我试过很多种模板后保留下来最实用的一个。
| 复盘维度 | 关键问题 | 典型风险 |
|---|---|---|
| 来源完整性 | 所有预期的数据源都对齐了吗?有没有遗漏的日志流或数据库表? | 源表字段变更未通知,导致管道断裂 |
| 时序对齐 | 不同源的数据时间戳是否一致?时区处理是否正确? | 毫秒级时间戳与秒级时间戳混用,造成特征错位 |
| 采样策略 | 全量采集还是采样?采样策略是否引入了偏差? | 随机采样去掉了罕见的但重要的极端样本 |
| 版本控制 | 数据集的版本和代码版本能否对应上? | 线上模型用的是V2数据,实验却用V1,结果完全不可比 |
在复盘日志时,我通常会先跑一条命令,快速概览一下原始数据的基本情况。
# 快速概览日志数量和时间范围
wc -l raw_logs/*.log
head -1 raw_logs/20250401.log
tail -1 raw_logs/20250430.log
这里有个坑——日志里经常夹杂着一些奇怪的字符或空行。我曾经在一次复盘中发现,某个日志文件在每日轮转时出现了重复的行,导致后续统计全乱套了。所以,简单的数量计算只是第一步,你必须结合字段级别的质量检查。
3.2 数据质量评估:量化你的脏数据
数据干不干净,不是靠感觉的。我习惯用一套通用的质量指标来量化,这样复盘时大家有据可依,不会吵架。
数据质量七大指标
- 完整性 —— 缺失值的比例。超过5%的字段需要特别关注。
- 准确性 —— 值与真实情况的偏差。比如年龄字段出现负值,那就是红线。
- 一致性 —— 同一实体的值在不同源中是否冲突。同一个人身份证号码,A表是18位,B表是15位,这事得查。
- 时效性 —— 数据是否在有效时间范围内。过期的特征会导致模型产生错误的关联。
- 唯一性 —— 重复记录的占比。重复样本在训练时会变相增加权重,影响模型公平性。
- 有效性 —— 值是否符合业务规则。比如性别字段不能填“阿巴阿巴”。
- 均匀性 —— 分布是否合理。长尾分布下,头部数据可能掩盖尾部模式。
这些指标不是空话。我手里有一份当时项目的实际质量报告片段,给你看看具体的实现。你可以把它当模板,放到自己的复盘文档里。
表名:user_behavior_log
总记录数:1,234,567
缺失率:
- device_id: 0.02%
- click_time: 0.00%
- page_url: 1.23% <-- 注意这个字段
重复率:
- 完全重复行: 2,345 (0.19%)
- 基于(user_id, click_time)的重复: 1,098 (0.09%)
异常值:
- click_time 中未来时间戳: 12 条
- page_url 为空字符串: 321 条
一致性检查:
- user_id 在 user_profile 表中存在率: 98.76%
- 存在 1.24% 的 user_id 未关联到任何用户画像
你看,数据是不会骗人的。当缺失率超过1%时,你就得问一句:这些缺失值背后有没有系统性的原因?是采集程序写错了,还是业务本身就有跑单的用户?这些信息,是后面清洗策略的决策依据。
3.3 清洗策略复盘:动作要留痕,结果要可复现
清洗数据时,最忌讳的就是直接用Excel打开文件手动改。我从不允许团队这么干。为什么?因为你改完之后,没人知道你改了哪里、改了多久、为什么这么改。这种脏活一旦没了记录,复盘就成了猜谜游戏。
我的做法是:每个清洗操作,都必须有对应的代码和日志。清洗程序必须是幂等的,跑一次和跑一百次,结果一模一样。这样复盘时,只要重跑一遍清洗管道,就能复现当时的处理流程。
3.3.1 常见的清洗动作归类
复盘时,把清洗动作分门别类整理,能帮你发现规律。比如,某个阶段突然出现大量异常值清洗,往往意味着上游接口发生了变化。
| 清洗类别 | 典型动作 | 复盘关注点 |
|---|---|---|
| 缺失值处理 | 填充、删除、标记 | 填充策略是否基于领域知识?还是直接用了均值? |
| 异常值过滤 | 截断、盖帽法、分箱 | 过滤阈值是怎么定的?有没有过度清洗掉有意义的极端样本? |
| 重复数据删除 | 完全去重、基于主键去重 | 重复是业务允许的重复,还是采集缺陷? |
| 格式统一 | 时间格式、数值单位、编码 | 转换规则是否覆盖了所有边缘情况? |
| 业务规则校验 | 逻辑约束、依赖关系 | 规则版本是否与当前业务版本一致? |
一个实用小技巧
清洗代码里一定要加一个 is_raw_data 的参数。设为 True 时,从原始数据重新跑一遍完整流程。这样无论何时复盘,你都能保证结果的一致性。我因为这个习惯,救过两次项目复盘会。
看一段实际的清洗代码片段,注意我是怎么保留清洗日志的。
说实话,这段代码没什么炫技的地方,但贵在清晰。每处理一步,都记录下被处理了多少行。这样复盘时,你就能精确地知道:“哦,原来在缺失值填充这一步,丢掉了12%的数据。然后异常值截断又动了2%的范围。”
def clean_behavior_data(df):
"""清洗用户行为数据,并记录清洗日志。"""
log = {}
original_rows = len(df)
# 1. 删除完全重复行
df = df.drop_duplicates()
log['duplicates_removed'] = original_rows - len(df)
# 2. 填充缺失的device_id
before_fill = df['device_id'].isna().sum()
df['device_id'] = df['device_id'].fillna('unknown_device')
log['device_id_filled'] = before_fill
# 3. 过滤未来时间戳
max_time = pd.Timestamp.now()
future_mask = df['click_time'] > max_time
log['future_timestamps_removed'] = future_mask.sum()
df = df[~future_mask]
# 4. 删除空page_url
empty_url_mask = df['page_url'].str.strip().eq('') | df['page_url'].isna()
log['empty_url_removed'] = empty_url_mask.sum()
df = df[~empty_url_mask]
return df, log
警告:版本混沌陷阱
我曾经在复盘时遇到一个极其头疼的情况:团队里有人用Pandas 1.4跑的清洗,有人用2.0跑的。某个fillna的默认行为在两版之间发生了变化!结果导致同一份原始数据,跑出两份不同的清洗后数据。排查了一整天才找到原因。
所以,清洗环境的依赖必须锁定:pandas==2.0.3,numpy==1.24.2。用requirements.txt或conda env export固定下来。
3.4 工具与标准化:构建可复用的复盘流水线
如果每次复盘都手工搞一套清洗脚本,那就太累了。我建议在项目初期就搭建一个标准化的数据收集与清洗管道。这个管道本身也应该是复盘内容的一部分。
下面这张图概括了数据清洗复盘的核心流程,你看看能不能匹配你的项目情况。
这个流程里,我最看重的是 “可重放” 。即,给你一份新的原始数据,按相同的清洗管道执行,必须得到相同质量的结果。复盘既是对过去工作的检查,也是在为下一次项目铺经验。
3.4.1 清洗工具的标准化选型
不同项目用不同工具,但在复盘阶段,我们需要统一的视角。我比较推荐用Pandas或PySpark做核心处理,配合Great Expectations做质量断言。你也可以用Deequ、Dolt之类的工具,但关键是团队统一。
标准化选型后,复盘报告里可以直接引用质检断言的结果。比如用Great Expectations检查关键字段:
expect_column_values_to_not_be_null("user_id")
expect_column_values_to_be_between("age", min_value=0, max_value=120)
expect_column_values_to_match_regex("email", r"^[^@]+@[^@]+\.[^@]+$")
断言不通过的地方,就是清洗策略需要微调的地方。这份质检报告本身,就是你复盘时的最强论据。