一、问题的起点:预警为什么需要「归因」
很多流失预警系统做到这样一步就停了:
- 用户连续 N 天没有交易 → 标记为流失预警
- 按风险等级分个「高 / 中 / 低」→ 推给客服去跟进
但客服拿到一条预警时,心里想的是另一件事:
「我知道这个客户要流失了,但他为什么要流失?」
如果客服只能看到一行冷冰冰的「连续 12 天无消费」,他跟进时还得自己翻交易流水、猜原因。而「归因诊断」(Attribution Diagnosis)要做的,就是把「他为什么流失」这件事自动化、结构化地算出来,直接告诉客服:
- 「交易额骤降 65%」
- 「日均笔数由 8 笔快速回落至仅 2 笔」
这才是预警真正能落地的关键一环。
二、数据建模:一张「日流水汇总表」打底
归因的本质是对比:本月 vs 上月,现状 vs 历史。要支撑这种对比,最合适的数据形态不是原始交易流水,而是一张按天 + 按用户预聚合的汇总表。
在我们的实现里,它是cn_user_daily_flow_summary:
| 字段 | 含义 |
|---|---|
user_id | 用户 |
stat_date | 统计日期 |
total_expense | 日支出(交易额) |
transaction_count | 交易笔数 |
income_count/expense_count | 收款 / 支出次数 |
有了这张表,「本月交易额」「上月日均笔数」这类指标都能用一次SUM聚合算出来,而不必回原始流水表扫全量数据。
设计要点:预聚合表把「实时算不动」的问题,提前变成「可秒级查询」的问题。这是归因诊断能低延迟返回的前提。
三、指标设计:三个能「说人话」的标签
归因诊断的输出要能让客服直接念出来,而不是丢一堆数字。我们沉淀出三个指标:
1. 交易额骤降 X%
对比本月与上月的交易额,降幅越大说明流失信号越强:
降幅 = (上月交易额 - 本月交易额) / 上月交易额 × 100%2. 日均笔数由 A 笔快速回落至仅 B 笔
光看总额会被大额单笔交易干扰,所以补一个「笔数」维度:
日均笔数 = 月度总交易笔数 / 当月天数当月均笔数从上月的 A 掉到本月的 B(且 A > 0、B < A)时,说明交易频率在衰减。
3. 流失概率(辅助字段)
配合预警等级一起用,把「还要多久流失」量化出来:
- 连续无消费 ≥ 15 天 → 高危,预计 15 天内流失
- ≥ 7 天 → 中危
- ≥ 3 天 → 低危
三个指标各管一块:额度、频率、紧迫度,拼起来就是一条完整归因。
四、实现中踩过的三个坑
坑 1:COUNT(*)≠ 交易笔数
这是最隐蔽、也最容易算错的一个。
最初写「本月笔数」时,有人顺手用了COUNT(*):
sqlSELECT user_id, SUM(total_expense) AS total_expense, COUNT(*) AS expense_count FROM cn_user_daily_flow_summary WHERE ... GROUP BY user_id看着没问题,但cn_user_daily_flow_summary是按天一行的汇总表。COUNT(*)数出来的其实是「有流水的天数」,不是「交易笔数」。
一个用户本月有 30 天流水、每天 100 笔,COUNT(*)返回 30,而真实笔数是 3000——差了 100 倍。
正确的写法是用汇总表里真正的笔数字段:
sqlSELECT user_id, SUM(total_expense) AS total_expense, SUM(transaction_count) AS tx_count FROM cn_user_daily_flow_summary WHERE ... GROUP BY user_id教训:聚合表里能
COUNT(*)的对象,取决于表的粒度。先想清楚「一行代表什么」,再决定用COUNT还是SUM。
坑 2:日均笔数的「天数口径」
算日均时,分母不能图省事用「30」或「本月有流水的天数」。
- 用
30:2 月只有 28 天,会被系统性高估。 - 用「有流水的天数」:分子分母不同口径,失真。
正确做法是取自然月的实际天数:
javaint currDays = LocalDate.now().lengthOfMonth(); // 本月天数 int lastDays = LocalDate.now().minusMonths(1).lengthOfMonth(); // 上月天数 double currAvg = (double) currTx / currDays; double lastAvg = (double) lastTx / lastDays;坑 3:空值与边界条件
归因标签不能无条件生成。比如:
- 上月交易额为 0:没有可比的基数,不该生成「骤降 X%」。
- 上月笔数为 0:同上,不该生成「笔数回落」。
- 两个标签都不满足:返回空列表,而不是硬塞写死的占位文案。
我们把标签生成收敛成一个纯函数,边界条件都显式判断:
javaprivate List<String> buildBehaviorTags(BigDecimal currExp, BigDecimal lastExp, int currTx, int lastTx) { List<String> tags = new ArrayList<>(); // 交易额骤降:必须有上月基数,且本月确实下降 if (lastExp.compareTo(BigDecimal.ZERO) > 0 && currExp.compareTo(lastExp) < 0) { int drop = lastExp.subtract(currExp) .divide(lastExp, 4, BigDecimal.ROUND_HALF_UP) .multiply(BigDecimal.valueOf(100)).intValue(); tags.add("交易额骤降" + drop + "%"); } // 日均笔数回落:上月日均 > 0 且本月低于上月 if (lastAvg > 0 && currAvg < lastAvg) { tags.add("日均笔数由" + fmt(lastAvg) + "笔快速回落至仅" + fmt(currAvg) + "笔"); } return tags; // 可能为空,前端自然不展示 }五、总结
归因诊断把「流失预警」从告警升级成了可行动的洞察。回头看,它没那么玄,本质就是三件事:
- 一张对的分析表——按天预聚合的流水汇总,让对比查询可秒级返回。
- 三个能说人话的指标——额度、频率、紧迫度,拼成完整归因。
- 对粒度和边界较真——
COUNT(*)的粒度陷阱、日均的天数口径、空值边界,这些细节决定了标签是「可信」还是「误导」。
一个预警系统能不能真正帮到一线客服,往往不取决于它能不能发现风险,而取决于它能不能把风险解释清楚。这,就是归因诊断的价值。