主题
第3章 数据基础与模拟数据
3.1 概述
所有模型都建立在数据之上。在真实场景中,数据来自用户行为埋点、订单系统和券平台。但在开发和学习阶段,我们需要一套逼真的模拟数据来驱动整个流程。
本章会解答三个问题:
- 系统需要哪些数据表,每张表长什么样?
- 模拟数据的生成逻辑——如何让假数据"像真的"?
- 数据中隐含的因果结构——这对后续的 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_id | string | 用户唯一标识,格式 u_000001 |
register_time | datetime | 注册时间 |
device_os | string | 操作系统:iOS / Android / Web |
device_brand | string | 设备品牌:Apple / Huawei / Xiaomi / ... |
device_price_tier | string | 设备价位档:budget / mid / flagship |
channel_source | string | 获客渠道:organic / paid_search / social / ... |
city_tier | string | 城市等级:1-5 线 |
has_bindcard | int | 是否绑定银行卡:0/1 |
order_count | int | 历史订单数 |
total_gmv | float | 历史总 GMV |
浏览日志 (browse_logs)
| 字段 | 类型 | 说明 |
|---|---|---|
user_id | string | 用户 ID |
item_id | string | 商品 ID |
category_id | string | 品类 ID |
item_price | float | 商品价格 |
timestamp | datetime | 浏览时间 |
page_type | string | 页面类型:list / detail / cart / search / promo |
session_id | string | 会话 ID |
duration_sec | int | 停留时长(秒) |
实验数据 (experiment_data)
| 字段 | 类型 | 说明 |
|---|---|---|
user_id | string | 用户 ID |
treatment | int | 实验组标记:1=发券,0=不发券 |
converted | int | 是否转化:1=购买,0=未购买 |
gmv | float | 转化 GMV(未转化为 0) |
coupon_amount | int | 发放的券面额 |
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)))
...关键设计:
- 品类偏好集中:每个用户只关注 2-3 个品类,70% 的浏览落在偏好品类上。这模拟了真实用户"有目的地逛"的行为
- Session 结构:浏览事件按 session 分组,每个 session 内的事件时间间隔 5-60 秒
- 页面类型分布:detail 35% > list 30% > search 15% > cart 10% > promo 10%,符合漏斗结构
- 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 模型能学到增量效果的基础
- 价格敏感度标签从浏览行为中启发式构造,融合价格变异系数、促销页占比、搜索占比
思考题
- 为什么实验数据中 treatment/control 的分组必须是随机的?如果按某个特征(比如城市等级)来分组会怎样?
- 模拟数据中 Persuadable 用户占 20%。在真实电商场景中,你认为这个比例大约是多少?为什么?
- 敏感度标签的构造方式是启发式的。如果有真实的券核销数据,你会如何构造更精确的标签?