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

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_idcontent_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 数据管线与存储 采样、去重、补数这些动作发生在哪一层

✅ 检查点

  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 服务的」——它服务谁,就会在谁需要的时候被改,而那个人从来不是你。五种来源:业务日志(为对账排障)/ 埋点(为报表 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

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