🏠 总目录📚 本教程 02 · 数据是怎么来的 ← →
📑 本页目录(点开跳转)

02 · 一份数据集是怎么来的

⏱ 46 分钟 | ⭐ 五种来源,五套坑


🎯 一句话

你手里那张表不是「数据」,是某个人为了另一个目的、在某个时刻、按某一版口径写下来的东西。 把它当成「客观记录」,是数据工作里最贵的一个误解 —— 尤其是埋点:它是产品团队写的,口径随时会变,而没人会通知你。


🧭 一、五种来源,一眼看清

来源 谁产的 为什么产 你最该担心什么
业务日志 后端服务 对账、排障、审计 补写、重放、软删除、状态是最终态不是当时态 ⭐
埋点 产品 + 客户端 看报表、算 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. 看到一份标注数据,必须问清六件事:
  2. 标注指南是哪一版?现在线上用的是哪一版? ⭐(第1章那个事故)
  3. 每条标了几遍?一致性(Kappa)是多少? (第10章)
  4. 标注员是谁?外包还是业务专家?流失率多少?
  5. 样本是怎么抽的?随机还是按某种规则挑的? (第15章)
  6. 有没有"不确定"这个选项?它被怎么处理了?
  7. 标注时标注员能看到什么?会不会看到答案? ⚠️ 泄漏
  8. ⭐ 六个问题里,第①和第②决定了这份数据的【天花板】:
  9. 标注员之间都对不齐的东西,模型不可能学对。

💰 六、外购数据:三个必问,一个必查

必问 为什么
口径与生成方式 「行业标签」是人打的还是模型打的?如果是模型打的,你在做知识蒸馏而不是学习真实世界 ⭐
覆盖率与更新频率 覆盖率 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 数据管线与存储 采样、去重、补数这些动作发生在哪一层

✅ 检查点

  1. 五种数据来源分别是什么?各自「为谁的 KPI 服务」?
  2. 业务日志的三个坑是什么?为什么「状态表」不能直接当特征?
  3. 埋点要穿过哪三层?每层分别能改什么、会不会通知你?
  4. 埋点漂移的四种形态里,哪一种 schema 校验完全拦不住?为什么它最难查?
  5. 采样率变化为什么危险?为什么「按 user_id 哈希采样」会让用户级特征看起来正常?
  6. 口径指纹为什么要看绝对量而不是比率?diff_fingerprint 里哪两个信号最强?
  7. 拿到一份人工标注要问哪六件事?哪两件决定天花板?
  8. 外购数据的「一个必查」是什么?怎么查?
  9. 合成数据的「真假判别器」检验怎么做?AUC 接近 1 说明什么?SMOTE 必须在哪里做?
  10. 曝光口径那个事故:改了什么、连锁反应是什么、为什么六项校验全过、发现延迟多久、代价多少?最该记住的一句话是什么?
👀 答案
  1. 业务日志(后端服务,为对账排障)、埋点(产品+客户端,为报表 KPI)、人工标注(标注团队,唯一为你产的)、外购(第三方,为卖钱)、合成(你自己)。判断法:「这一列是为谁的 KPI 服务的」—— 它服务谁,就会在谁需要的时候被改。
  2. 状态是最终态不是当时态、补写与重放、软删除。状态表存的是最终结果(如 status='已退款'),你要预测的是下单那一刻——直接 join 就是用了未来,属于泄漏。正解是用变更流水表,或记录 valid_from/valid_to。
  3. ①客户端 SDK(客户端开发改上报时机、参数、事件名,不会通知你)、②网关/采集层(数据工程改采样率、去重、丢弃规则,可能通知)、③数仓 ODS→DWD(数仓改重命名、口径重算、补数,偶尔通知)。你看到的表是三层叠加的产物,表结构没变不代表①②没变。
  4. 上报时机变化(如曝光从「进入视口」改成「停留 ≥1s」)。因为字段名没变、类型没变、非空率没变,行数只是少了一些——所有自动校验全过,而含义已经换了一个。
  5. 因为它是一个安静的乘数(聚合类特征全部 ÷10 而你不知道),而且采样往往不是随机的:按 user_id 哈希取模只保留 10% 的用户,所以用户级特征分布看起来完全正常,但全站聚合特征全错。更糟的是采样率会在大促临时调整再调回,历史数据里一段 10% 一段 100%。
  6. 因为只监控比率的话,分母被换掉会让比率变"好看"(曝光口径收紧 → CTR 上升 → 以为模型进步了);只有同时盯分子分母的绝对值才能看见分母被换掉。最强的两个信号:行数突变和众数变了(众数换人几乎从不是正常波动,通常是枚举漂移或口径重定义)。
  7. ①指南哪一版/线上哪一版 ②标了几遍、Kappa 多少 ③标注员是谁 ④样本怎么抽的 ⑤有没有「不确定」选项 ⑥标注员能不能看到答案。①和②决定天花板——标注员之间都对不齐的东西,模型不可能学对。
  8. 查外部数据里有没有混进你的目标。查法:把外部特征单独拿出来训一个模型,如果单它一个就能到 0.85+,几乎一定有问题。典型案例:买的「企业风险评分」本身用了「是否已逾期」做输入,AUC 从 0.76 跳到 0.91,上线后崩到 0.68。
  9. 把真实数据标 1、合成数据标 0 训一个分类器:AUC≈0.5 说明合成得好,AUC≈0.99 说明合成数据有明显指纹,模型会学到「这是合成的」这件事本身。SMOTE 必须在训练集内部、且在交叉验证的每一折内部做 —— 先 SMOTE 再切分会让合成样本的父样本在训练集、子样本在验证集,指标虚高。
  10. 产品把曝光从「进入视口」改成「停留 ≥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 服务的」——它服务谁,就会在谁需要的时候被改,而那个人从来不是你。

业务日志三坑里最贵的一条:⚠️ 状态表存的是最终态不是当时态——直接拿「已退款」当特征就是用了未来。

⭐ 埋点是本章重点:它穿过客户端 SDK、采集层、数仓三层,而你看到的表是三层叠加的产物——⚠️ 表结构没变,不代表前两层没变。 漂移四形态里,改名和类型 schema 能拦,💀 而「上报时机变化」完全拦不住:字段名没变、类型没变、非空率没变,所有自动校验全过,含义却换了一个。

⚠️ 采样率是安静的乘数:按用户哈希采样会让用户级特征分布正常、全站聚合特征全错。

⭐ 对策是口径指纹:每天给每个字段拍一组分布数字然后 diff——关键是看绝对量不看比率(只看点击率的话,曝光口径收紧反而会让它变好看)。最强的两个信号是行数突变和众数换人。

⚠️ 外购数据必查一条:单独拿它训一个模型,如果单它一个就到很高的 AUC,⭐ 几乎一定是你的目标混进去了。

💀 那个事故:产品把曝光口径从「进入视口」改成「停留一秒」,字段名和类型全没改,而算法团队不在通知群里。

下一节 👉 03-数据契约.md

打卡记录保存在你的浏览器里,首页能看到总进度