4、流量分配策略设计:按用户ID哈希、按时间片轮转、按分层抽样

流量分配策略是授信引擎A/B测试的核心环节,决定了实验组与对照组能否在统计意义上保持同质性。不合理的分配策略会导致样本偏差、时间效应混淆或组间不均衡,最终使实验结论失效。本节详细阐述三种主流分配策略的原理、适用场景及实现要点。

4.1 按用户ID哈希分配

按用户ID哈希是最常用的确定性分配方法。通过对用户唯一标识(如用户ID、手机号、设备ID)进行哈希运算,将用户均匀映射到不同的实验组中。同一用户在不同时间进入实验时,始终被分配到同一组,保证了用户体验的一致性。

核心原理: hash(user_id) mod N → 实验组编号。其中N为实验组数,hash函数需保证输出均匀分布。

4.1.1 实现步骤

  1. 选择哈希函数:推荐使用MurmurHash3、MD5或SHA-256,避免使用简单的取模运算(如user_id % N),防止用户ID规律性导致分配偏斜。
  2. 确定分桶数:通常将流量分为100或1000个桶,再按比例映射到实验组。例如:实验组分配30%流量,则取哈希值 mod 1000,结果在[0, 299]范围内的用户进入实验组。
  3. 实现分配逻辑:在授信引擎入口处,对每个请求的用户ID执行哈希取模,根据结果路由到对应的实验版本。

4.1.2 优缺点对比

维度 说明
优点 用户一致性高,同一用户始终在同一组;分配速度快,O(1)时间复杂度;支持大规模流量,无状态存储需求。
缺点 用户ID分布不均匀时可能导致组间样本量偏差;新增实验组或调整比例时,已分配用户可能被重新分桶(需使用“盐值”或分层哈希解决)。

4.1.3 代码示例

import hashlib

def assign_group(user_id: str, total_buckets: int = 1000, experiment_ratio: float = 0.3) -> str:
    # 使用MD5哈希,取前8位转换为整数
    hash_val = int(hashlib.md5(user_id.encode()).hexdigest()[:8], 16)
    bucket = hash_val % total_buckets
    if bucket < int(total_buckets * experiment_ratio):
        return "experiment"
    else:
        return "control"
实践建议:在哈希前拼接“盐值”(如实验名称+版本号),可避免不同实验间的分配冲突,同时支持同一用户参与多个正交实验。
按用户ID哈希分配流程 用户请求(含user_id) hash(user_id) mod 1000 桶编号 对照组(70%) 实验组(30%) 同一用户始终进入同一组,保证体验一致性

4.2 按时间片轮转分配

时间片轮转分配以固定时间窗口(如每小时、每天)为单位,将整个时间片内的所有流量全部分配给某一实验组。这种方式适用于无法实时获取用户ID的场景,或需要模拟“自然实验”效果的情况。

风险提示:时间片轮转容易引入时间效应偏差。如果实验期间外部环境(如节假日、营销活动、系统更新)发生变化,不同时间片的用户行为差异可能被错误归因于实验策略。

4.2.1 分配规则

  • 固定轮转:按预定义的时间表切换,例如:周一、三、五为实验组,周二、四、六为对照组。
  • 随机轮转:每个时间片开始时,随机决定该片分配给哪个组,但需保证长期比例符合设定。
  • 周期长度:建议以“天”为单位,避免小时级轮转导致昼夜效应混淆。

4.2.2 适用场景

场景 说明
匿名流量 无法获取用户ID(如未登录访客、API匿名调用),无法使用哈希分配。
后端服务实验 服务端无用户上下文,仅按请求时间路由到不同版本。
快速验证 需要快速上线实验,且对用户一致性要求不高。

4.2.3 代码示例

import datetime

def assign_by_time_slice(experiment_start: datetime.date, cycle_days: int = 7) -> str:
    today = datetime.date.today()
    days_since_start = (today - experiment_start).days
    # 每cycle_days为一个轮转周期,前70%天数走对照组,后30%走实验组
    position_in_cycle = days_since_start % cycle_days
    if position_in_cycle < int(cycle_days * 0.7):
        return "control"
    else:
        return "experiment"
按时间片轮转分配示意(7天周期) D1 D2 D3 D4 D5 D6 D7 对照组 对照组 对照组 对照组 对照组 实验组 实验组 每个时间片内所有流量进入同一组,简单但易受时间效应影响 一个完整轮转周期(7天)→ 对照组:实验组 = 5:2

4.3 按分层抽样分配

分层抽样分配先按关键特征(如用户风险等级、新老客、渠道来源)将用户划分为若干层,然后在每一层内独立进行随机分配。这种方法能保证各实验组在关键特征上的分布与总体一致,减少组间差异,提升统计灵敏度。

核心优势:分层抽样可以控制已知的混杂变量,使实验组和对照组在重要维度上保持均衡,从而降低方差,提高实验的统计功效。

4.3.1 分层变量选择

分层变量 原因 分层示例
用户风险等级 授信额度、通过率与风险等级高度相关 低风险、中风险、高风险
新老客标识 新老客的授信行为模式差异显著 新客(注册≤30天)、老客
渠道来源 不同渠道的用户质量不同 自有APP、第三方导流、线下

4.3.2 实现流程

  1. 定义分层变量:选择与核心指标(如授信通过率、逾期率)相关的特征。
  2. 划分层级:每个分层变量取若干离散值,组合成多个层。例如:风险等级(3) × 新老客(2) = 6个层。
  3. 层内分配:在每个层内,使用哈希或随机数将用户分配到实验组或对照组,比例保持一致。
  4. 汇总统计:各层独立计算指标后,按层权重加权汇总得到整体效果。

4.3.3 代码示例

import hashlib

def stratified_assign(user_id: str, risk_level: str, is_new_user: bool) -> str:
    # 构建分层键
    layer_key = f"{risk_level}_{'new' if is_new_user else 'old'}"
    # 在层内使用哈希分配
    hash_val = int(hashlib.md5((user_id + layer_key).encode()).hexdigest()[:8], 16)
    bucket = hash_val % 100
    if bucket < 30:  # 每层30%进入实验组
        return "experiment"
    else:
        return "control"
注意事项:分层不宜过细,否则某些层样本量过少,反而增加方差。建议每层至少包含100个样本。同时,分层变量必须在实验前确定,不能在实验过程中根据结果调整。
分层抽样分配结构 总体流量 低风险层 中风险层 高风险层 对照组(70%) 对照组(70%) 对照组(70%) 实验组(30%) 实验组(30%) 实验组(30%) 按层权重加权汇总 → 整体实验效果 每层内独立分配,保证各层组间均衡,降低混杂变量影响

4.4 三种策略对比与选型建议

策略 用户一致性 抗时间效应 实现复杂度 适用场景
用户ID哈希 高 强 低 有用户ID、需长期追踪的场景
时间片轮转 低 弱 最低 匿名流量、快速验证
分层抽样 高 强 中 需控制混杂变量、提升统计功效
选型原则:优先使用“用户ID哈希+分层抽样”的组合策略。在哈希分配的基础上,对关键特征进行分层,既能保证用户一致性,又能控制已知混杂变量。时间片轮转仅作为无法获取用户ID时的备选方案,且需配合时间效应校正方法使用。