4. 数据准备:历史数据采集、数据清洗、特征对齐、时间窗口划分

数据准备是 Champion Challenger 回放评估的基石。原始日志通常来自生产环境,包含噪声、缺失值、时间偏差和特征不一致。本章将系统讲解如何将原始日志转化为可用于评估的标准化数据集,涵盖采集、清洗、对齐与窗口划分四个核心阶段。

4.1 历史数据采集

采集阶段的目标是获取 Champion 与 Challenger 模型在相同生产时段内的完整决策日志。采集范围需覆盖以下三类数据源:

数据源 采集内容 关键字段
模型服务日志 请求时间、模型版本、输入特征、输出分数/标签 request_id, model_version, score, features
业务结果日志 用户后续行为、转化、留存、收入等真实结果 request_id, conversion, revenue, timestamp
实验分流日志 用户/请求被分配到的实验组标识 user_id, experiment_id, variant
采集避坑:务必确保 Champion 与 Challenger 的日志时间戳使用同一时钟源(如 NTP 同步),避免因服务器时钟偏差导致后续时间窗口错位。

4.2 数据清洗

原始日志中常见的脏数据包括重复请求、缺失特征、异常时间戳和实验组标记错误。清洗流程按以下步骤执行:

  1. 去重:以 request_id 为唯一键,保留首次出现的记录,删除后续重复行。
  2. 缺失值处理:对数值型特征使用中位数填充,对类别型特征使用众数填充;若缺失率超过 50%,直接丢弃该特征。
  3. 异常值过滤:剔除特征值超出 3 倍标准差范围或明显不符合业务逻辑的记录(如年龄 < 0 或 > 120)。
  4. 实验组校验:检查分流日志中 Champion 与 Challenger 的样本量比例是否接近预期(如 50:50),偏差超过 5% 需排查分流逻辑。
注意:清洗阶段不要删除 Champion 与 Challenger 之间的差异样本,只处理明显的数据错误。过度清洗可能抹掉模型之间的真实效果差异。

4.3 特征对齐

Champion 与 Challenger 模型可能使用不同的特征版本或特征工程逻辑。特征对齐的目的是确保评估时两个模型在相同的特征空间上可比。对齐操作包括:

  • 特征名称映射:统一两个模型的特征命名规范,例如 Champion 的 user_age 与 Challenger 的 age 应映射为同一特征。
  • 特征值归一化:若 Champion 使用原始值、Challenger 使用对数变换,需统一为同一尺度(如 Z-score 标准化)。
  • 缺失特征补齐:若 Challenger 引入了新特征,Champion 的历史日志中无此特征,则用默认值(如 0 或均值)填充。
对齐原则:特征对齐不是让两个模型的特征完全相同,而是让评估数据集中的每条记录都包含两个模型在该样本上的完整特征副本,以便后续计算得分差异。

4.4 时间窗口划分

时间窗口划分用于将连续日志切分为独立的评估单元,避免时间序列自相关对统计检验造成干扰。常用的划分策略有三种:

窗口类型 划分方式 适用场景
固定时长窗口 按固定时间长度(如 1 小时、1 天)切分 流量均匀、无明显周期性波动的场景
事件驱动窗口 按业务事件(如每次模型上线、每次数据刷新)切分 模型版本频繁更新、业务节奏不固定的场景
滑动窗口 固定窗口长度 + 固定步长滑动(如窗口 7 天、步长 1 天) 需要细粒度监控趋势变化的场景

以下 SVG 展示了从原始日志到评估数据集的完整数据准备流程:

数据准备全流程 历史数据采集 服务日志·业务日志·分流日志 数据清洗 去重·缺失填充·异常过滤 特征对齐 名称映射·归一化·补齐 时间窗口划分 固定时长窗口 | 事件驱动窗口 | 滑动窗口 评估数据集 包含 Champion 与 Challenger 的完整特征、得分、标签、窗口 ID 每个窗口内的样本独立用于后续统计检验,窗口间不交叉

4.5 窗口划分的工程实现

以下 Python 代码示例展示了如何使用 Pandas 对日志数据执行固定时长窗口划分:

import pandas as pd

def assign_fixed_window(df, window_minutes=60):
    """
    按固定时间窗口为每条记录分配窗口ID。
    
    参数:
        df: 包含 'timestamp' 列的 DataFrame
        window_minutes: 窗口长度(分钟)
    
    返回:
        添加了 'window_id' 列的 DataFrame
    """
    df = df.copy()
    df['timestamp'] = pd.to_datetime(df['timestamp'])
    # 以最早时间为基准,计算窗口编号
    base_time = df['timestamp'].min()
    df['window_id'] = ((df['timestamp'] - base_time).dt.total_seconds() // (window_minutes * 60)).astype(int)
    return df

# 使用示例
logs = pd.read_parquet('model_logs.parquet')
logs_with_window = assign_fixed_window(logs, window_minutes=60)
print(logs_with_window[['request_id', 'timestamp', 'window_id']].head())
实践建议:窗口大小选择需平衡统计功效与时间分辨率。窗口过小(如 1 分钟)会导致每个窗口内样本量不足,统计检验不稳定;窗口过大(如 1 周)则无法捕捉短期效果波动。推荐从业务周期(如 1 天)开始尝试,再根据样本量调整。

4.6 数据准备检查清单

在进入评估环节前,请逐项确认以下检查点:

  • ✅ Champion 与 Challenger 的样本量比例在预期范围内(如 50:50 ± 5%)
  • ✅ 无重复 request_id,无关键特征缺失率超过 50% 的列
  • ✅ 两个模型的特征名称已统一映射,缺失特征已补齐
  • ✅ 时间窗口划分后,每个窗口内至少包含 100 条以上样本
  • ✅ 窗口 ID 已作为新列写入数据集,可用于后续分组统计

完成以上步骤后,即可获得一份干净、对齐、分窗的评估数据集,为后续的统计检验与效果对比奠定坚实基础。