Skip to content

第3章 数据基础与模拟数据 ​

3.1 概述 ​

所有模型都建立在数据之上。在真实场景中,数据来自用户行为埋点、订单系统和券平台。但在开发和学习阶段,我们需要一套逼真的模拟数据来驱动整个流程。

本章会解答三个问题:

  1. 系统需要哪些数据表,每张表长什么样?
  2. 模拟数据的生成逻辑——如何让假数据"像真的"?
  3. 数据中隐含的因果结构——这对后续的 Uplift 模型至关重要

3.2 数据表设计 ​

整个系统依赖四张核心数据表:

mermaid
erDiagram
    USERS ||--o{ BROWSE_LOGS : "产生浏览"
    USERS ||--o| EXPERIMENT_DATA : "参与实验"
    USERS ||--o| SENSITIVITY_LABELS : "敏感度标注"

    USERS {
        string user_id PK
        datetime register_time
        string device_os
        string device_brand
        string device_price_tier
        string channel_source
        string city_tier
        int has_bindcard
        int order_count
        float total_gmv
    }

    BROWSE_LOGS {
        string user_id FK
        string item_id
        string category_id
        float item_price
        datetime timestamp
        string page_type
        string session_id
        int duration_sec
    }

    EXPERIMENT_DATA {
        string user_id FK
        int treatment
        int converted
        float gmv
        int coupon_amount
    }

    SENSITIVITY_LABELS {
        float sensitivity
    }

用户表 (users) ​

字段类型说明
user_idstring用户唯一标识,格式 u_000001
register_timedatetime注册时间
device_osstring操作系统:iOS / Android / Web
device_brandstring设备品牌:Apple / Huawei / Xiaomi / ...
device_price_tierstring设备价位档:budget / mid / flagship
channel_sourcestring获客渠道:organic / paid_search / social / ...
city_tierstring城市等级:1-5 线
has_bindcardint是否绑定银行卡:0/1
order_countint历史订单数
total_gmvfloat历史总 GMV

浏览日志 (browse_logs) ​

字段类型说明
user_idstring用户 ID
item_idstring商品 ID
category_idstring品类 ID
item_pricefloat商品价格
timestampdatetime浏览时间
page_typestring页面类型:list / detail / cart / search / promo
session_idstring会话 ID
duration_secint停留时长(秒)

实验数据 (experiment_data) ​

字段类型说明
user_idstring用户 ID
treatmentint实验组标记:1=发券,0=不发券
convertedint是否转化:1=购买,0=未购买
gmvfloat转化 GMV(未转化为 0)
coupon_amountint发放的券面额

3.3 模拟数据生成 ​

模拟数据不是简单的随机数。好的模拟数据要包含业务逻辑和因果结构,否则后续模型训练出来的结果没有参考价值。

用户生成 ​

python
<!-- scripts/generate_sample_data.py -->
def generate_users(n: int = 50_000, seed: int = 42) -> pd.DataFrame:
    rng = np.random.default_rng(seed)
    user_ids = [f"u_{i:06d}" for i in range(n)]

    # 注册时间:指数分布,大部分是近期注册
    register_offsets = rng.exponential(scale=72, size=n)  # 小时

    # 设备分布:反映真实市场份额
    device_os = rng.choice(
        ["iOS", "Android", "Web"],
        size=n,
        p=[0.35, 0.55, 0.10]
    )

    # 订单数:大部分新用户为 0
    order_count = np.zeros(n, dtype=int)
    has_order = rng.random(n) < 0.15  # 15% 有过订单
    order_count[has_order] = rng.integers(1, 5, size=has_order.sum())

    # GMV:对数正态分布(长尾)
    total_gmv = np.zeros(n)
    total_gmv[has_order] = rng.lognormal(
        mean=4.5, sigma=0.8, size=has_order.sum()
    )
    ...

关键设计决策:

  • 注册时间用指数分布:scale=72(小时),意味着大部分用户是最近 3 天内注册的,符合新用户池的时间分布
  • 设备分布参照真实市场:Android 55%、iOS 35%、Web 10%
  • GMV 用对数正态分布:电商 GMV 天然呈长尾分布——少数高客单价用户贡献大部分 GMV
  • 85% 新用户没有订单:这才是新用户数据的真实面貌

浏览日志生成 ​

浏览日志的生成更讲究,需要模拟真实的用户行为模式:

python
<!-- scripts/generate_sample_data.py -->
def generate_browse_logs(users, avg_events=30, seed=42):
    rng = np.random.default_rng(seed)
    categories = [f"cat_{i:02d}" for i in range(20)]

    for _, user in users.iterrows():
        n_events = max(0, int(rng.poisson(avg_events)))

        # 每个用户有偏好品类(集中在 2-3 个)
        preferred_cats = rng.choice(categories, size=3, replace=False)
        cat_probs = np.array([0.5, 0.3, 0.2])

        # 每个用户有偏好价格区间(对数正态)
        price_center = rng.lognormal(mean=4.0, sigma=0.8)

        # 按 session 组织浏览行为
        n_sessions = max(1, int(rng.poisson(3)))
        ...

关键设计:

  1. 品类偏好集中:每个用户只关注 2-3 个品类,70% 的浏览落在偏好品类上。这模拟了真实用户"有目的地逛"的行为
  2. Session 结构:浏览事件按 session 分组,每个 session 内的事件时间间隔 5-60 秒
  3. 页面类型分布:detail 35% > list 30% > search 15% > cart 10% > promo 10%,符合漏斗结构
  4. Poisson 分布:浏览量用泊松分布(均值 30),自然产生"活跃用户多浏览、沉默用户少浏览"的差异

实验数据生成 ​

这是最关键的部分——实验数据中必须包含真实的因果结构:

python
<!-- scripts/generate_sample_data.py -->
def generate_coupon_experiment_data(users, seed=42):
    rng = np.random.default_rng(seed)
    n = len(users)

    # 随机分组(50/50)
    treatment = np.zeros(n, dtype=int)
    indices = rng.permutation(n)
    treatment[indices[:n // 2]] = 1

    # 基础转化率 ~8%,发券组平均提升 ~3%
    base_cvr = 0.08
    treatment_lift = 0.03

    # 用户特征影响基础转化率
    device_bonus = np.where(users["device_os"] == "iOS", 0.02, 0.0)
    bindcard_bonus = users["has_bindcard"].values * 0.03
    city_bonus = np.where(
        np.isin(users["city_tier"].values.astype(str), ["1", "2"]),
        0.02, 0.0
    )

    base_prob = base_cvr + device_bonus + bindcard_bonus + city_bonus
    treat_prob = base_prob + treatment * treatment_lift

    # 20% 的用户是 Persuadables,券效果更大
    persuadable = rng.random(n) < 0.2
    treat_prob[persuadable & (treatment == 1)] += 0.08
    ...

因果结构的设计:

基础转化概率 = 8% + 设备加成 + 绑卡加成 + 城市加成
发券增量     = 3%(平均)
Persuadable 额外增量 = 8%(仅对 20% 的用户)

这意味着:

  • 普通用户发券后转化率从 ~8% 提升到 ~11%(增量 3%)
  • Persuadable 用户发券后转化率从 ~8% 提升到 ~19%(增量 11%)
  • iOS + 绑卡 + 一线城市的用户基础转化率就有 ~15%

这个设计让 Uplift 模型有"信号"可以学:不是所有用户发券效果一样大,Persuadable 用户的增量效果显著更强。

价格敏感度标签 ​

python
<!-- scripts/generate_sample_data.py -->
def generate_sensitivity_labels(users, browse_logs, seed=42):
    # 基于浏览行为的启发式标签
    user_stats = browse_logs.groupby("user_id").agg(
        price_std=("item_price", "std"),
        price_mean=("item_price", "mean"),
        promo_ratio=("page_type", lambda x: (x == "promo").mean()),
        search_ratio=("page_type", lambda x: (x == "search").mean()),
    )

    # 价格变异系数 + 促销页占比 + 搜索占比 → 综合敏感度
    cv = (user_stats["price_std"] / user_stats["price_mean"].clip(lower=1))
    sensitivity = 0.4 * cv + 0.3 * promo_ratio + 0.3 * search_ratio
    return sensitivity.clip(0, 1)

标签构造逻辑:价格敏感度没有"真值",需要从行为中推断。三个信号:

  • 价格变异系数(40%权重):比价行为越多,敏感度越高
  • 促销页浏览占比(30%权重):常逛促销页,说明价格敏感
  • 搜索占比(30%权重):搜索行为多,说明目标明确、比较价格

3.4 运行数据生成 ​

bash
python scripts/generate_sample_data.py --n-users 50000 --output-dir data

输出:

生成 50000 个模拟用户...
  → data/users.csv (50000 rows)
生成浏览日志...
  → data/browse_logs.csv (~1,500,000 rows)
生成AB实验数据...
  → data/experiment_data.csv (50000 rows)
生成价格敏感度标签...
  → data/sensitivity_labels.csv (50000 rows)
✅ 数据生成完成!

3.5 数据质量检查 ​

生成数据后,建议做几个快速检查:

python
import pandas as pd

users = pd.read_csv("data/users.csv")
browse = pd.read_csv("data/browse_logs.csv")
exp = pd.read_csv("data/experiment_data.csv")

# 1. 用户表
print(f"用户数: {len(users)}")
print(f"有订单用户: {(users['order_count'] > 0).mean():.1%}")
print(f"设备分布:\n{users['device_os'].value_counts(normalize=True)}")

# 2. 浏览日志
print(f"\n浏览记录数: {len(browse)}")
print(f"人均浏览: {len(browse) / len(users):.0f}")
print(f"页面类型:\n{browse['page_type'].value_counts(normalize=True)}")

# 3. 实验数据
print(f"\nTreatment/Control 比例: "
      f"{exp['treatment'].mean():.1%} / {1 - exp['treatment'].mean():.1%}")
print(f"总体转化率: {exp['converted'].mean():.1%}")
print(f"发券组转化率: {exp[exp['treatment']==1]['converted'].mean():.1%}")
print(f"对照组转化率: {exp[exp['treatment']==0]['converted'].mean():.1%}")

预期输出应该是:

  • Treatment/Control 接近 50/50
  • 发券组转化率比对照组高约 3-5 个百分点
  • 人均浏览量约 30 条

本章小结 ​

  • 系统依赖四张核心数据表:用户表、浏览日志、实验数据、敏感度标签
  • 模拟数据需要包含业务逻辑(品类偏好、Session 结构)和因果结构(Persuadable 用户)
  • 关键分布选择:注册时间用指数分布、GMV 用对数正态分布、浏览量用泊松分布
  • 实验数据中 20% 的用户是 Persuadable,这是 Uplift 模型能学到增量效果的基础
  • 价格敏感度标签从浏览行为中启发式构造,融合价格变异系数、促销页占比、搜索占比

思考题 ​

  1. 为什么实验数据中 treatment/control 的分组必须是随机的?如果按某个特征(比如城市等级)来分组会怎样?
  2. 模拟数据中 Persuadable 用户占 20%。在真实电商场景中,你认为这个比例大约是多少?为什么?
  3. 敏感度标签的构造方式是启发式的。如果有真实的券核销数据,你会如何构造更精确的标签?

基于 Uplift Modeling 的电商优惠券智能发放系统