📑 本页目录(点开跳转)
02 · 一份数据集是怎么来的
⏱ 52 分钟 | ⭐ 五种来源,五套坑
🎯 一句话
你手里那张表不是「数据」,是某个人为了另一个目的、在某个时刻、按某一版口径写下来的东西。 把它当成「客观记录」,是数据工作里最贵的一个误解 —— 尤其是埋点:它是产品团队写的,口径随时会变,而没人会通知你。
🧭 一、五种来源,一眼看清
| 来源 | 谁产的 | 为什么产 | 你最该担心什么 |
|---|---|---|---|
| 业务日志 | 后端服务 | 对账、排障、审计 | 补写、重放、软删除、状态是最终态不是当时态 ⭐ |
| 埋点 | 产品 + 客户端 | 看报表、算 KPI | 口径随时变,且不通知你 ⭐⭐ |
| 人工标注 | 标注团队 | 就是给你用的 | 指南哪一版、一致性多少、标注员是谁 |
| 外购/外部 | 第三方 | 卖钱 | 口径不透明、覆盖率、里面混着你的答案 ⚠️ |
| 合成 | 你自己 | 补稀缺样本 | 边缘分布像,联合分布和因果结构不像 ⚠️ |
🔑 一条贯穿全章的判断法: 拿到任何一列,先问 「这一列是为谁的 KPI 服务的?」 答案几乎从来不是「为你的模型」。 它服务谁,就会在谁需要的时候被改。
📜 二、业务日志:为对账写的,不是为你写的
业务日志通常是最"可信"的一类 —— 因为钱要对得上。但它的设计目标决定了三个坑。
坑 1:状态表存的是【最终态】,不是【当时态】 ⭐⭐
订单表里 status = '已退款'
但你要预测的是「下单那一刻会不会退款」
→ 如果你直接 join 订单表当特征,你用了未来 → 泄漏(第14章)
✅ 正解:用【变更流水表】而不是【状态快照表】
没有流水表?那就必须记录每个字段的 valid_from / valid_to
坑 2:补写与重放
支付回调失败会重试,日志里同一笔出现 3 次
对账任务夜里会补写昨天缺的记录 → 昨天的数据今天才全
→ 如果你 T+0 就取数,永远少一截 ⚠️
坑 3:软删除
is_deleted = 1 的行还躺在表里
很多人写 SQL 时忘了加过滤 → 训练集里混着已撤销的业务
怎么发现:
[ ] 表里有没有 update_time?它和 create_time 差多少?
—— 差得越多,说明这行被改过越多次,越不能当"当时态"用 ⭐
[ ] 按天统计行数,看最近 3 天是不是明显偏低(= 还在补写)
[ ] grep 一遍所有 is_deleted / is_valid / status 类的列,
确认你的 SQL 都过滤了
[ ] 主键真的唯一吗?(第 6 章)
📡 三、埋点:本章的重点 ⭐⭐
埋点不是数据工程产的,是产品经理提需求、客户端开发写的。 它的生命周期完全由产品迭代驱动,而你的模型,不在任何人的考虑范围内。
3.1 埋点要穿过三层,每层都能改
①客户端 SDK ②网关/采集层 ③数仓 ODS→DWD
───────────── ───────────── ──────────────
谁改:客户端开发 谁改:数据工程 谁改:数仓
改什么: 改什么: 改什么:
· 上报时机 ⭐⭐ · 采样率 ⭐ · 字段重命名
· 参数增删 · 去重策略 · 口径重算
· 事件名合并 · 丢弃规则 · 补数/回溯
通知你吗:不会 通知你吗:可能 通知你吗:偶尔
⭐ 你在第 ③ 层看到的表,是三层改动叠加后的产物。
表结构没变,不代表①②没变。
3.2 埋点漂移的四种,按恶心程度排序
| 形态 | 例子 | schema 校验能拦住吗 |
|---|---|---|
| 字段删除 / 改名 | item_id → content_id |
✅ 能,任务直接挂 |
| 类型变化 | price 从 string 变 int |
✅ 多半能 |
| 枚举新增值 | channel 多了 mini_program |
⚠️ 不能(第 4、3 章) |
| 上报时机变化 💀 | 「曝光」从进入视口 → 停留 ≥1s | ❌ 完全不能 |
💀 「上报时机变化」是所有数据事故里最难查的一类: 字段名没变、类型没变、非空率没变、行数只是「少了一些」—— 所有自动校验全部通过, 而这一列的含义已经换了一个。
3.3 采样率:一个安静的乘数
埋点上报量太大 → 数据团队开采样:只上报 10%
→ 你的"曝光次数"特征全部 ÷10(但你不知道)
→ 更糟的是:采样往往【不是随机的】
常见做法是按 user_id 哈希取模 → 只保留 10% 的【用户】
→ 用户级特征的分布没变,但【全站聚合特征】全错 ⭐
⚠️ 而且采样率会随大促临时调整,事后再调回来
→ 你的历史数据里有一段是 10%,一段是 100%
🧪 四、口径指纹:抓「schema 没变但语义变了」
既然 schema 校验拦不住时机变化和采样变化,就需要另一套东西: 每天给每个关键字段拍一张「指纹」,然后比昨天。
import numpy as np
import pandas as pd
def fingerprint(df: pd.DataFrame, cols, day_col="dt"):
"""给每个 (日期, 字段) 算一组分布指纹。schema 不变但语义变了,会在这里露出来。"""
out = []
for day, g in df.groupby(day_col):
for c in cols:
s = g[c]
rec = {"dt": day, "col": c, "n": len(s), "null_rate": s.isna().mean()}
if pd.api.types.is_numeric_dtype(s):
q = s.quantile([0.05, 0.5, 0.95]).values
rec.update({"p05": q[0], "p50": q[1], "p95": q[2],
"zero_rate": (s == 0).mean()})
else:
vc = s.value_counts(normalize=True)
rec.update({"n_uniq": s.nunique(),
"top1": vc.index[0] if len(vc) else None,
"top1_share": vc.iloc[0] if len(vc) else np.nan})
out.append(rec)
return pd.DataFrame(out)
def diff_fingerprint(fp: pd.DataFrame, ref_day, cur_day, tol=0.15):
"""比较两天的指纹,列出可疑变化。tol 是相对变化阈值。"""
a = fp[fp.dt == ref_day].set_index("col")
b = fp[fp.dt == cur_day].set_index("col")
alerts = []
for c in a.index.intersection(b.index):
# ⭐ 绝对量的变化最能暴露"上报时机变了"——比率类指标会互相抵消,看不出来
if abs(b.loc[c, "n"] - a.loc[c, "n"]) / max(a.loc[c, "n"], 1) > tol:
alerts.append((c, "行数突变", a.loc[c, "n"], b.loc[c, "n"]))
for k in ("null_rate", "p50", "p95", "zero_rate", "top1_share", "n_uniq"):
if k in a.columns and pd.notna(a.loc[c, k]) and pd.notna(b.loc[c, k]):
base = abs(a.loc[c, k]) + 1e-9
if abs(b.loc[c, k] - a.loc[c, k]) / base > tol:
alerts.append((c, k, a.loc[c, k], b.loc[c, k]))
# ⭐ 众数换人 = 枚举漂移或口径重定义,几乎从不是正常波动
if "top1" in a.columns and a.loc[c, "top1"] != b.loc[c, "top1"]:
alerts.append((c, "众数变了", a.loc[c, "top1"], b.loc[c, "top1"]))
return pd.DataFrame(alerts, columns=["col", "signal", "ref", "cur"])
# 用法:fp = fingerprint(df, ["impression", "price", "channel"]); diff_fingerprint(fp, "2026-03-11", "2026-03-12")
⭐
diff_fingerprint里最关键的设计是「看绝对量,不看比率」: 如果只监控 CTR,曝光口径收紧会让 CTR 变好看,你还以为模型进步了。 只有同时盯分子和分母的绝对值,才能看见分母被换掉。 (这条和 模型上线之后 11 的「分母变了」是同一件事的两端。)
✍️ 五、人工标注:唯一为你而生的数据,也是最贵的
看到一份标注数据,必须问清六件事:
① 标注指南是哪一版?现在线上用的是哪一版? ⭐⭐(第1章那个事故)
② 每条标了几遍?一致性(Kappa)是多少? (第10章)
③ 标注员是谁?外包还是业务专家?流失率多少?
④ 样本是怎么抽的?随机还是按某种规则挑的? (第15章)
⑤ 有没有"不确定"这个选项?它被怎么处理了?
⑥ 标注时标注员能看到什么?会不会看到答案? ⚠️ 泄漏
⭐ 六个问题里,第①和第②决定了这份数据的【天花板】:
标注员之间都对不齐的东西,模型不可能学对。
💰 六、外购数据:三个必问,一个必查
| 必问 | 为什么 |
|---|---|
| 口径与生成方式 | 「行业标签」是人打的还是模型打的?如果是模型打的,你在做知识蒸馏而不是学习真实世界 ⭐ |
| 覆盖率与更新频率 | 覆盖率 60% 意味着 40% 缺失,且缺的那部分通常不是随机的(第 5 章 MNAR) |
| 许可与合规边界 | 能不能用于模型训练、能不能跨境、留存多久(第 19 章) |
一个必查:
⚠️ 外部数据里有没有混进你的目标?
典型:买了一份"企业风险评分"来做信贷模型
结果这份评分本身就用了【是否已逾期】做输入
→ 你的模型 AUC 从 0.76 跳到 0.91
→ 上线后崩到 0.68(新客户还没逾期,评分是空的)
✅ 查法:把外部特征单独拿出来训一个模型,
如果【单它一个】就能到 0.85+,几乎一定有问题(第14章)
🧬 七、合成数据:边缘像,联合不像
最常见的三种合成:
① 规则生成(faker、模板) —— 分布完全由你的想象决定
② 过采样/SMOTE —— ⚠️ 在特征空间插值,可能造出物理上不存在的样本
③ 生成模型(GAN/扩散/LLM) —— 边缘分布很像,但【条件依赖结构】常常错
⭐ 一个必做的检验:训练一个"真假判别器"
把真实数据标 1、合成数据标 0,训一个分类器
· AUC ≈ 0.5 → 合成得很好
· AUC ≈ 0.99 → 合成数据有明显指纹,模型会学到"这是合成的"这件事本身
⚠️ SMOTE 有个几乎人人都踩的坑: 必须在训练集内部做,且在交叉验证的每一折内部做。 先 SMOTE 再切分 = 合成样本的"父样本"在训练集、"子样本"在验证集 → 指标虚高(第 6 章、第 14 章会详细说这个机制)。
(合成数据的完整讨论在 第 16 章。)
💀 八、事故复盘:曝光口径改了一行,模型偏了 19 天
发生了什么
系统:信息流推荐的 CTR 预估模型,日均请求 2.6 亿
训练数据:曝光日志 × 点击日志 join,曝光为负样本、点击为正样本
3 月 12 日,产品上线了一个"曝光质量优化":
曝光定义: item 进入视口 (旧)
→ item 在视口内停留 ≥1s (新)
目的:让报表里的 CTR 更"真实",对齐竞品口径
埋点字段名:impression —— 没改
埋点参数: 类型、必填性 —— 没改
通知了谁:产品群、数据看板群 —— 算法团队不在里面 ⚠️
为什么没被发现
| 校验项 | 3/11 | 3/12 | 通过? |
|---|---|---|---|
| 字段存在 | ✅ | ✅ | 通过 |
| 类型一致 | int | int | 通过 |
| 非空率 | 100% | 100% | 通过 |
| 主键唯一 | ✅ | ✅ | 通过 |
| 日行数波动阈值 ±40% | 4.21 亿 | 2.78 亿 | 通过(-34%,没超 40%)💀 |
| CTR | 4.1% | 6.2% | 没有人告警 ——因为它变"好"了 💀💀 |
⭐ 训练侧的连锁反应:
曝光(负样本)少了 34%,点击(正样本)几乎没变
→ 正样本比例 4.1% → 6.2%,涨了 51%
→ 模型预估的绝对值整体抬高约 1.5 倍
→ 而下游的【竞价出价】和【冷启动流量阈值】是按绝对预估值配的固定阈值
→ 阈值全部失效:低质内容被判定为"高预估 CTR",拿到了大量流量
怎么发现的
不是监控发现的。 是 3 月 31 日一个运营在群里问: 「为什么最近推荐的东西越来越水?」
发现延迟 = 19 天
⚠️ 而模型的所有技术指标在这 19 天里都是正常甚至变好的:
离线 AUC 0.712 → 0.719(因为正负比更平衡,AUC 反而好看)
线上 CTR 报表 4.1% → 6.3%(分母变小)
服务成功率、延迟、特征缺失率 —— 全部正常
代价
19 天 × 2.6 亿请求 ≈ 49 亿次受影响的曝光
低质内容分发占比从 3.2% 涨到 11.7%
次日留存 -0.9pp(事后用断点回归估的,见《模型上线之后》13)
重建训练集 + 回溯 60 天曝光 + 重训 + 灰度:14 人日
⭐ 真正的修复只用了 14 人日;19 天全花在"没人知道"上
该补什么
| 缺失 | 补上之后 |
|---|---|
| 算法团队不在埋点变更通知链里 | 数据契约里必须有消费者清单,改动前必须通知(第 3 章)⭐⭐ |
| 只监控比率,不监控绝对量 | 曝光数、点击数各自单独告警;比率类指标必须同时展示分子分母 ⭐ |
| 行数阈值 ±40% 太松 | 改成按分位数的动态阈值 + 连续 2 天同向偏移即告警 |
| 「指标变好」不触发任何检查 | CTR 单日跳变 > 20%(无论方向)一律告警(第 8 章)⭐⭐ |
| 模型输出的绝对值没被监控 | 预估分的分位数进监控(模型上线之后 05) |
⭐⭐ 这个事故最该记住的一句话: 「指标变好了」从来不是「不用查」的理由。 在数据这一关,向上的跳变和向下的跳变同样危险 —— 而绝大多数监控体系只对向下的那一半设了告警。
🗺️ 九、一张来源溯源表(建议每个项目都建一份)
| 字段 | 来源层 | 产出方 | 口径定义链接 | 更新频率 | 最近变更 | 我们的用途 |
|------|--------|--------|-------------|---------|---------|-----------|
| impression | 埋点 | 客户端-Feed组 | wiki/xxx | 实时 | 2026-03-12 ⚠️ | 负样本 |
| order_amt | 业务 | 交易服务 | wiki/yyy | T+1 | 2025-11-03 | 特征 |
| risk_score | 外购 | XX数据 | 合同附件3 | 月更 | 未知 ⚠️ | 特征 |
⭐ 「最近变更」那一列如果写着"未知",就是一个待办事项。
⭐ 这张表本身就是第 3 章数据契约的雏形,也是第 17 章血缘的人工版。
🔗 这一章连到哪里
| 去哪 | 为什么 |
|---|---|
| 推荐算法 03 数据长什么样 | 那边具体展示曝光/点击日志的结构;本章讲这些日志是怎么被改掉的 |
| 模型上线之后 06 数据漂移与概念漂移 | 漂移是「世界变了」,本章的口径变更是「记录世界的方式变了」—— 现象一样,处理完全不同 ⭐ |
| 模型上线之后 11 从告警到根因 | 「分母变了」那一条就是本章事故的排查视角 |
| AI 基础设施 22 数据管线与存储 | 采样、去重、补数这些动作发生在哪一层 |
✅ 检查点
- 五种数据来源分别是什么?各自「为谁的 KPI 服务」?
- 业务日志的三个坑是什么?为什么「状态表」不能直接当特征?
- 埋点要穿过哪三层?每层分别能改什么、会不会通知你?
- 埋点漂移的四种形态里,哪一种 schema 校验完全拦不住?为什么它最难查?
- 采样率变化为什么危险?为什么「按 user_id 哈希采样」会让用户级特征看起来正常?
- 口径指纹为什么要看绝对量而不是比率?
diff_fingerprint里哪两个信号最强? - 拿到一份人工标注要问哪六件事?哪两件决定天花板?
- 外购数据的「一个必查」是什么?怎么查?
- 合成数据的「真假判别器」检验怎么做?AUC 接近 1 说明什么?SMOTE 必须在哪里做?
- 曝光口径那个事故:改了什么、连锁反应是什么、为什么六项校验全过、发现延迟多久、代价多少?最该记住的一句话是什么?
👀 答案
- 业务日志(后端服务,为对账排障)、埋点(产品+客户端,为报表 KPI)、人工标注(标注团队,唯一为你产的)、外购(第三方,为卖钱)、合成(你自己)。判断法:「这一列是为谁的 KPI 服务的」—— 它服务谁,就会在谁需要的时候被改。
- 状态是最终态不是当时态、补写与重放、软删除。状态表存的是最终结果(如
status='已退款'),你要预测的是下单那一刻——直接 join 就是用了未来,属于泄漏。正解是用变更流水表,或记录valid_from/valid_to。 - ①客户端 SDK(客户端开发改上报时机、参数、事件名,不会通知你)、②网关/采集层(数据工程改采样率、去重、丢弃规则,可能通知)、③数仓 ODS→DWD(数仓改重命名、口径重算、补数,偶尔通知)。⭐你看到的表是三层叠加的产物,表结构没变不代表①②没变。
- 上报时机变化(如曝光从「进入视口」改成「停留 ≥1s」)。因为字段名没变、类型没变、非空率没变,行数只是少了一些——所有自动校验全过,而含义已经换了一个。
- 因为它是一个安静的乘数(聚合类特征全部 ÷10 而你不知道),而且采样往往不是随机的:按 user_id 哈希取模只保留 10% 的用户,所以用户级特征分布看起来完全正常,但全站聚合特征全错。更糟的是采样率会在大促临时调整再调回,历史数据里一段 10% 一段 100%。
- 因为只监控比率的话,分母被换掉会让比率变"好看"(曝光口径收紧 → CTR 上升 → 以为模型进步了);只有同时盯分子分母的绝对值才能看见分母被换掉。最强的两个信号:行数突变和众数变了(众数换人几乎从不是正常波动,通常是枚举漂移或口径重定义)。
- ①指南哪一版/线上哪一版 ②标了几遍、Kappa 多少 ③标注员是谁 ④样本怎么抽的 ⑤有没有「不确定」选项 ⑥标注员能不能看到答案。①和②决定天花板——标注员之间都对不齐的东西,模型不可能学对。
- 查外部数据里有没有混进你的目标。查法:把外部特征单独拿出来训一个模型,如果单它一个就能到 0.85+,几乎一定有问题。典型案例:买的「企业风险评分」本身用了「是否已逾期」做输入,AUC 从 0.76 跳到 0.91,上线后崩到 0.68。
- 把真实数据标 1、合成数据标 0 训一个分类器:AUC≈0.5 说明合成得好,AUC≈0.99 说明合成数据有明显指纹,模型会学到「这是合成的」这件事本身。SMOTE 必须在训练集内部、且在交叉验证的每一折内部做 —— 先 SMOTE 再切分会让合成样本的父样本在训练集、子样本在验证集,指标虚高。
- 产品把曝光从「进入视口」改成「停留 ≥1s」,字段名和类型都没改,算法团队不在通知链里。连锁反应:曝光(负样本)-34%,点击几乎不变 → 正样本率 4.1%→6.2%(+51%)→ 预估值整体抬高约 1.5 倍 → 下游按绝对值配的竞价出价和冷启阈值全部失效。六项校验全过是因为字段/类型/非空/主键都没变,日行数 -34% 没超 ±40% 阈值,而 CTR 从 4.1% 涨到 6.2% 没人告警,因为它变"好"了。发现延迟 19 天,靠运营一句「为什么推荐的越来越水」。代价:约 49 亿次曝光受影响、低质分发占比 3.2%→11.7%、次日留存 -0.9pp,而修复只要 14 人日。⭐⭐最该记住的:「指标变好了」从来不是「不用查」的理由——向上的跳变和向下的跳变同样危险。
🛑 可以停在这里
⚡ 走神救援
⭐核心:你手里那张表不是「数据」,是某个人为了另一个目的、在某个时刻、按某一版口径写下来的东西。 判断法:问「这一列是为谁的 KPI 服务的」——它服务谁,就会在谁需要的时候被改,而那个人从来不是你。五种来源:业务日志(为对账排障)/ 埋点(为报表 KPI)/ 人工标注(唯一为你产的)/ 外购(为卖钱)/ 合成。业务日志三坑:⭐⭐状态表存的是最终态不是当时态(
status='已退款'直接当特征就是用了未来 → 泄漏,正解是用变更流水表或 valid_from/valid_to)、补写与重放(回调重试 + 夜里补数,T+0 取数永远少一截)、软删除(忘了过滤 is_deleted)。⭐⭐埋点是本章重点:它穿过三层——①客户端 SDK(改上报时机/参数,不通知你)②采集层(改采样率,可能通知)③数仓(改命名/口径,偶尔通知);你看到的表是三层叠加的产物,表结构没变不代表①②没变。漂移四形态里,改名和类型变 schema 能拦,枚举新增值拦不住,而💀「上报时机变化」完全拦不住——字段名没变、类型没变、非空率没变,所有自动校验全过,含义却换了一个。⚠️采样率是安静的乘数:按 user_id 哈希采样会让用户级特征分布正常、全站聚合特征全错,而且大促会临时调再调回。对策是⭐口径指纹:每天给每个字段拍 n / null_rate / p05-p50-p95 / zero_rate / 众数占比,然后 diff;关键是看绝对量不看比率——只看 CTR 的话,曝光口径收紧会让它变好看;最强的两个信号是行数突变和众数换人。标注要问六件事,其中指南版本和一致性 Kappa 决定天花板。外购必查一条:⚠️外部数据里有没有混进你的目标——单独拿它训一个模型,单它一个就到 0.85+ 几乎一定有问题(买的风险评分里含「是否已逾期」,AUC 0.76→0.91,上线崩到 0.68)。合成数据用真假判别器验:AUC≈0.5 好,≈0.99 说明有指纹;⚠️SMOTE 必须在每一折训练集内部做,否则父样本在训练集、子样本在验证集。💀事故:3/12 产品把曝光从「进入视口」改成「停留 ≥1s」,字段名类型全没改,算法团队不在通知群里。结果曝光 -34%、点击不变 → 正样本率 4.1%→6.2%(+51%)→ 预估值整体抬高约 1.5 倍 → 下游按绝对值配的竞价出价和冷启阈值全失效。六项校验全过:字段/类型/非空/主键都正常,日行数 -34% 没超 ±40% 阈值,而 CTR 变好看了所以没人告警;离线 AUC 甚至从 0.712 涨到 0.719。发现延迟 19 天,靠运营问「为什么推荐的越来越水」;代价 49 亿次曝光、低质分发 3.2%→11.7%、次日留存 -0.9pp,修复只要 14 人日。补救:契约里写消费者清单、绝对量单独告警、行数阈值改动态、⭐⭐CTR 单日跳变 >20% 无论方向一律告警。最该记住:「指标变好了」从来不是「不用查」的理由。
下一节 👉 03-数据契约.md