4、统计特征工程:滑动窗口统计量(均值、方差、Z-score)、时间序列特征(夜间交易占比、间隔时间)

在异常交易识别中,原始交易流水数据往往包含大量噪声和个体差异。统计特征工程的核心任务是通过数学变换,从原始数据中提取出能够反映账户行为模式、偏离程度以及时间规律的衍生特征。本章聚焦于两类最常用的统计特征:滑动窗口统计量(均值、方差、Z-score)和时间序列特征(夜间交易占比、间隔时间)。

4.1 滑动窗口统计量

滑动窗口(Rolling Window)是指在时间轴上固定一个长度(如过去7天、30笔交易),随着新数据的到来,窗口向前滑动,并实时计算窗口内数据的统计指标。这类特征能够捕捉账户行为的短期趋势与波动性变化。

4.1.1 核心指标定义

指标 计算公式 异常识别含义
滑动均值 μt = (1/n) Σi=t-n+1t xi 反映账户近期的平均交易水平。若当前交易金额远高于滑动均值,可能为异常大额交易。
滑动方差 σt2 = (1/n) Σ (xi - μt)2 衡量交易金额的波动剧烈程度。方差突然增大,说明账户行为出现不稳定,可能是试探性交易或盗刷。
Z-score Zt = (xt - μt-1) / σt-1 标准化后的偏离程度。|Z| > 3 通常被视为强异常信号,表示当前交易与历史模式严重不符。
工程避坑: 计算 Z-score 时,必须使用 前一个窗口 的均值和方差(即 μt-1 和 σt-1),避免引入未来信息(Look-ahead Bias)。同时,当窗口内方差为 0 时,需做除零保护,将 Z-score 置为 0 或一个极大值。

4.1.2 滑动窗口 Z-score 计算流程

下图展示了基于滑动窗口的 Z-score 异常检测逻辑:

滑动窗口 Z-score 异常检测流程 原始交易流 滑动窗口 (n=30) 计算 μ, σ 当前交易 xt Z = (xt - μ) / σ |Z| > 3 ? 异常告警 正常 说明:窗口大小 n 的选择需结合业务场景。 高频交易场景建议 n=50~100;低频场景建议 n=10~30。

4.2 时间序列特征

交易行为在时间维度上具有显著的周期性规律。异常交易往往表现为时间模式上的“不合群”,例如在凌晨批量转账、交易间隔极度均匀或极度混乱。以下两个特征在风控实践中极为有效。

4.2.1 夜间交易占比

定义:在指定时间窗口(通常为过去7天或30天)内,发生在 夜间时段(如 23:00 - 05:00) 的交易笔数占总交易笔数的比例。

  • 正常用户:夜间交易占比通常较低(< 5%),除非是夜班工作者或特定娱乐行业。
  • 异常账户:盗刷、洗钱、机器脚本交易常集中在夜间,占比可能超过 30% 甚至 80%。
  • 特征衍生:可进一步拆分为“夜间金额占比”和“夜间笔数占比”,两者结合可识别“小额高频夜间试探”行为。
实战经验: 夜间时段的定义应随地域调整。对于跨境交易场景,建议使用账户注册地或常用 IP 所在地的时区,而非统一使用 UTC+8。

4.2.2 交易间隔时间

定义:同一账户相邻两笔交易之间的时间差(单位:秒/分钟)。基于间隔时间可衍生出多个强特征:

衍生特征 计算方法 异常模式
平均间隔 窗口内所有间隔的均值 过短(< 1秒)可能为机器刷单;过长可能为账户休眠后突然激活。
间隔标准差 窗口内间隔的波动程度 标准差极小(间隔均匀)→ 脚本交易;标准差极大 → 行为混乱,可能为盗刷。
最小间隔 窗口内最短的间隔 若最小间隔 < 0.5秒,极大概率为自动化程序。
间隔熵 对间隔分桶后计算信息熵 熵值低 → 间隔模式单一(机器);熵值高 → 行为随机(人工)。

4.2.3 时间特征工程架构

下图展示了从原始交易时间戳到最终特征向量的完整加工链路:

时间序列特征加工链路 原始交易时间戳 提取小时/日期 计算间隔时间 夜间交易占比 间隔统计量 间隔熵 最终时间特征向量(输入模型)

4.3 特征工程代码示例(Python)

以下代码展示了如何基于 Pandas 快速计算滑动窗口 Z-score 和夜间交易占比:

import pandas as pd
import numpy as np

# 假设 df 包含字段:user_id, trans_time, amount
df['trans_time'] = pd.to_datetime(df['trans_time'])
df = df.sort_values(['user_id', 'trans_time'])

# 1. 滑动窗口 Z-score(窗口大小30笔)
df['rolling_mean'] = df.groupby('user_id')['amount'] \
    .transform(lambda x: x.shift(1).rolling(30, min_periods=10).mean())
df['rolling_std'] = df.groupby('user_id')['amount'] \
    .transform(lambda x: x.shift(1).rolling(30, min_periods=10).std())
df['z_score'] = (df['amount'] - df['rolling_mean']) / df['rolling_std'].replace(0, np.nan)

# 2. 夜间交易占比(过去7天)
df['hour'] = df['trans_time'].dt.hour
df['is_night'] = df['hour'].between(23, 5) | df['hour'].between(0, 5)

# 按用户+日期窗口聚合
night_ratio = df.set_index('trans_time').groupby('user_id')['is_night'] \
    .rolling('7D').mean().reset_index()
night_ratio.columns = ['user_id', 'trans_time', 'night_ratio_7d']
df = df.merge(night_ratio, on=['user_id', 'trans_time'], how='left')

# 3. 交易间隔特征
df['prev_time'] = df.groupby('user_id')['trans_time'].shift(1)
df['interval_sec'] = (df['trans_time'] - df['prev_time']).dt.total_seconds()

# 窗口内间隔统计
interval_stats = df.groupby('user_id')['interval_sec'] \
    .rolling(30, min_periods=5).agg(['mean', 'std', 'min']).reset_index()
interval_stats.columns = ['user_id', 'idx', 'interval_mean', 'interval_std', 'interval_min']
df = df.merge(interval_stats, left_index=True, right_on='idx', how='left')
重要提醒: 在计算滑动窗口特征时,务必使用 .shift(1) 排除当前交易自身,避免特征与目标变量直接相关导致过拟合。同时,对于新账户(冷启动),窗口内数据不足时,建议使用全局统计量或中位数进行填充,而非直接丢弃。