3. 特征计算任务定义与拆解
在逐笔特征批量计算中,最容易出问题的地方往往不是代码写不出来,而是“算什么”没有定义清楚。业务方一句“帮我看看这些交易有没有异常”,如果直接进入编码,结果通常是特征口径反复返工、窗口边界各写各的、上下游依赖混乱。本节要解决的核心问题,就是把模糊的业务需求,逐步收敛为一份可执行、可依赖、可拆分的特征计算任务清单。
从业务需求到特征清单:先翻译,再落表
业务需求通常以“判断目标”的形式出现,例如“识别短时间内高频分散转账的账户”“发现夜间大额且对手方集中的交易”。特征清单的构建,本质是把判断目标翻译成可计算的量化描述。建议按三步走:
- 拆判断条件:把“高频分散转账”拆成“频次高”“对手方数量多”“金额分散”三个可观测维度。
- 定计算口径:为每个维度确定统计对象(账户还是交易)、时间范围(如近1小时、近24小时)、聚合方式(计数、去重计数、求和、均值、标准差)。
- 落成特征条目:每条特征必须有唯一名称、口径说明、输入字段、输出类型和依赖的原始表。
以“近1小时高频分散转账”为例,可落成如下特征条目(示例参数,仅作教学说明):
| 特征名 | 口径说明 | 输入字段 | 输出类型 |
|---|---|---|---|
| txn_cnt_1h | 付款账户近1小时转出交易笔数 | payer_id, txn_time, amount | 整数 |
| opp_cnt_1h | 付款账户近1小时转出交易对手方去重数 | payer_id, payee_id, txn_time | 整数 |
| amt_std_1h | 付款账户近1小时转出金额标准差 | payer_id, txn_time, amount | 浮点 |
| night_amt_ratio_24h | 近24小时夜间(0–6点)转出金额占比 | payer_id, txn_time, amount | 浮点 |
这张表就是后续所有工作的“合同”。口径一旦确定,窗口边界、时区、金额单位、空值处理都要写进说明,避免不同开发人员对“近1小时”理解不一致——是左闭右开还是左闭右闭,是否包含当前这笔交易,必须明确。
单笔特征与窗口特征:两类计算对象
特征清单里的条目,按计算对象可分为两类,它们的实现方式和性能特征差异很大。
单笔特征只依赖当前这一笔交易自身的字段,逐行即可算出,例如交易金额是否超过阈值、交易时间是否落在夜间、付款方与收款方是否同城、金额是否为整数。这类特征无状态、可并行、易回刷,通常用简单的行级映射就能完成。
窗口特征则依赖当前交易前后一段时间内其他交易的信息,例如近1小时笔数、近7天对手方数量、近30天金额均值。它必须按主体(账户/卡号)和时间排序后做滑动聚合,涉及状态维护和窗口边界处理,是批量计算中开销最大的部分。
两者的关键区别在于:单笔特征“看自己”,窗口特征“看历史”。因此拆解任务时,应先把单笔特征与窗口特征分开,单笔特征走轻量通道,窗口特征单独设计聚合逻辑,避免混在一个作业里导致资源浪费和调试困难。
计算依赖图:让特征之间的先后关系显式化
特征之间并非彼此独立。有的特征直接由原始交易表聚合而来,有的特征则依赖其他特征的结果,例如“近1小时金额均值”可以由“近1小时金额总和”除以“近1小时笔数”得到。如果不把依赖关系画清楚,很容易出现重复计算或循环依赖。
构建依赖图的方法是:以原始数据表为根节点,每个特征为一个节点,若特征B的计算需要特征A的输出,则从A向B连一条有向边。图中应保证无环,并按拓扑顺序安排计算批次。下面这张图展示了一个简化的依赖关系(示例):
图中绿色为单笔特征,黄色为窗口聚合特征,紫色为派生特征。可以看到 amt_avg_1h 同时依赖 amt_sum_1h 和 txn_cnt_1h,必须先算完两个基础聚合才能计算;dispersion_score 则跨层依赖了单笔特征与窗口特征。依赖图一旦画出来,计算批次和并行度就有了依据。
任务粒度划分原则
依赖图给出的是逻辑关系,落到物理执行还需要划分任务粒度。粒度过粗,单个任务耗时过长、失败重跑代价大;粒度过细,调度开销和中间落盘成本上升。实践中可参考以下原则:
- 按依赖层级分批:同一拓扑层内的特征互不依赖,可并行计算;跨层必须串行。这是最稳妥的切分方式。
- 按主体分区:窗口特征通常按账户或卡号分区,同一主体的数据必须落在同一分区内,否则窗口聚合会跨分区出错。
- 按窗口长度分组:1小时、24小时、7天、30天的窗口计算成本差异大,建议按窗口长度拆成不同任务,便于分别调优和回刷。
- 控制单任务数据量:以单任务处理时长在可接受范围内为准(例如示例中控制在10–20分钟),超出则继续按时间片或主体哈希拆分。
一个可执行的拆解示例
仍以上述特征为例,按依赖层级和窗口长度可拆为四个任务(示例):
任务T1(单笔特征)
输入:txn, acct
输出:f_single(is_night, is_round_amt, same_city)
分区:按交易日期
任务T2(1小时窗口聚合)
输入:txn
输出:f_win_1h(txn_cnt_1h, amt_sum_1h, opp_cnt_1h)
分区:按 payer_id 哈希
窗口:左闭右开 [t-1h, t)
任务T3(派生特征)
输入:f_win_1h
输出:f_derived(amt_avg_1h, dispersion_score)
依赖:T2 完成后执行
任务T4(24小时窗口聚合)
输入:txn
输出:f_win_24h(night_amt_ratio_24h)
分区:按 payer_id 哈希
窗口:左闭右开 [t-24h, t)
这里把1小时与24小时窗口分开,是因为二者数据扫描范围和状态维护成本不同,分开后可以独立调度、独立回刷。T3 必须等 T2 完成,体现了依赖图的拓扑约束。窗口统一采用左闭右开区间,并在所有任务中保持一致,避免边界重复计数。
完成到这一步,特征计算任务就从一句业务描述,变成了有明确输入、输出、依赖、分区和窗口口径的可执行清单。后续的批量计算、流水线编排与回刷,都建立在这份清单之上。定义得越清楚,后面返工越少。