4. 数据准备与探索
Velocity 类特征工程的质量,几乎完全取决于数据准备阶段是否把“时间、主体、行为”三件事对齐。交易流水看似只是一张表,但它同时承载了用户、商户、设备、渠道、金额、状态与时间等多重语义。若在进入特征计算前没有完成结构梳理、维度拆分、时间戳标准化与脏数据识别,后续所有滑动窗口统计都会建立在错误前提上。本节围绕四个核心动作展开:理解流水结构、拆解用户与商户维度、统一时间语义、完成缺失与异常初筛。
一、核心原理:Velocity 特征对数据底座的三个硬要求
Velocity 速度特征的本质,是在“某个主体”上,按“某个时间窗口”统计“某类行为”的频次、金额、离散度或突变程度。因此数据底座必须满足三个硬要求:
- 主体可唯一标识:用户、商户、设备、卡号等主体必须有稳定 ID,且同一主体在不同表中可关联。
- 时间可比较:所有事件时间必须统一为同一时区、同一精度,并能区分“事件发生时间”与“入账/入库时间”。
- 行为可枚举:交易类型、状态、渠道、币种等字段必须可枚举、可过滤,否则窗口统计会把退款、撤销、失败交易混入正常消费。
二、交易流水数据结构:先看清字段语义,再谈特征
典型交易流水表通常包含以下字段族。理解它们的分工,比记住字段名更重要。
| 字段族 | 典型字段 | 在 Velocity 中的作用 |
|---|---|---|
| 交易标识 | trade_id、order_no、request_id | 去重、关联、追踪单笔事件 |
| 主体标识 | user_id、merchant_id、device_id、card_no | 定义窗口统计的“主体” |
| 时间字段 | event_time、create_time、pay_time、update_time | 决定窗口边界与顺序 |
| 金额与币种 | amount、currency、fee | 金额速度、金额突变、均值方差 |
| 状态与类型 | status、trade_type、channel | 过滤有效交易,区分行为类别 |
| 地理与设备 | ip、city、device_type、os | 多主体交叉速度、异常聚集 |
实际数据中,同一语义常有多个字段并存。例如“支付时间”可能同时存在 pay_time 与 create_time,二者相差数秒到数分钟。Velocity 特征应优先使用事件发生时间,而不是入库时间,否则窗口会被批处理延迟扭曲。
三、用户与商户维度:把一张宽表拆成可计算的主体视图
交易流水是“事件表”,而 Velocity 特征需要“主体-时间”视图。常见做法是保留事件粒度,同时补充主体属性。用户维度通常包括注册时间、实名状态、历史交易次数、风险等级;商户维度包括商户类别、注册时长、结算周期、历史投诉率。
关键原则是:主体属性只能使用事件发生前已知的信息。例如用“用户历史总交易次数”做特征时,必须按时间截断,不能把未来交易算进去,否则会造成标签泄漏。
-- 示例:按用户与商户双主体聚合近1小时交易速度
SELECT
user_id,
merchant_id,
COUNT(*) AS tx_cnt_1h,
SUM(amount) AS amt_sum_1h,
AVG(amount) AS amt_avg_1h,
MAX(event_time) AS last_tx_time
FROM trade_flow
WHERE status = 'SUCCESS'
AND event_time >= '2024-06-01 00:00:00'
AND event_time < '2024-06-02 00:00:00'
GROUP BY user_id, merchant_id;
四、时间戳处理:统一时区、精度与顺序
时间戳处理是本节最容易出错、也最影响特征稳定性的环节。建议按以下流程执行:
- 统一时区:将所有时间字段转换为 UTC 或业务统一时区,避免跨时区窗口错位。
- 统一精度:秒级与毫秒级混用时,先统一为毫秒或秒,防止窗口边界计算偏差。
- 区分时间语义:明确 event_time、create_time、pay_time 的先后关系,选择事件时间作为窗口基准。
- 排序与去重:按主体与事件时间排序,识别同一秒内重复上报或乱序数据。
- 生成窗口键:将事件时间映射到 1 分钟、5 分钟、1 小时等窗口桶,便于后续聚合。
# 示例:时间戳标准化与窗口键生成
df['event_time'] = pd.to_datetime(df['event_time'], utc=True)
df = df.sort_values(['user_id', 'event_time'])
df['event_ts'] = df['event_time'].astype('int64') // 10**9
df['win_1min'] = df['event_time'].dt.floor('1min')
df['win_1h'] = df['event_time'].dt.floor('1h')
五、缺失值与异常值初步分析:先识别,再决定处理策略
缺失与异常在交易数据中往往不是随机噪声,而是业务信号。例如设备号缺失可能来自模拟器或爬虫;金额为负可能是退款;时间戳为未来时间可能是伪造或时钟错误。建议先做统计画像,再分类处理。
- 缺失值:统计各字段缺失率,区分“业务允许缺失”与“异常缺失”。关键主体 ID 缺失通常应剔除或单独标记。
- 金额异常:检查负值、零值、极大值,结合币种与业务上限判断是否为测试交易或欺诈试探。
- 时间异常:识别未来时间、过早时间、同一主体时间倒序,判断是否需要修正或剔除。
- 重复交易:按 trade_id 或“主体+金额+时间”去重,避免速度被重复上报放大。
# 示例:缺失率与异常值快速画像
missing_rate = df.isnull().mean().sort_values(ascending=False)
print(missing_rate.head(10))
neg_amt = df[df['amount'] < 0]
future_ts = df[df['event_time'] > pd.Timestamp.now(tz='UTC')]
dup_trade = df[df.duplicated(subset=['trade_id'], keep=False)]
六、复盘:数据准备阶段最常见的三类问题
第一类是时间语义混淆,导致窗口统计与真实行为错位;第二类是主体 ID 不稳定,同一用户多 ID 或同一 ID 多用户,使速度被稀释或放大;第三类是脏数据未标记,退款、撤销、失败交易混入成功交易,使金额速度虚高。解决思路不是追求“绝对干净”,而是建立可追溯的数据准备流水线:字段语义明确、时间统一、主体可关联、异常可标记。只有底座对齐,后续 Velocity 特征才具备可解释性与稳定性。