8.4 KiB
亲密指数计算说明(代码翻译版)
1. 目的
本文档不是“业务口头定义”,而是按当前代码真实实现翻译出来的计算逻辑,便于你做以下事情:
- 跟业务同学对齐“现在系统到底怎么算的”
- 排查为什么某个客户-助教分数高/低
- 做参数调优前的影响评估
2. 代码入口与依赖
- 任务主类:
etl_billiards/tasks/dws/index/intimacy_index_task.py - 指数基类(衰减、分位、映射、平滑):
etl_billiards/tasks/dws/index/base_index_task.py - 课型映射(BASE/BONUS):
etl_billiards/tasks/dws/base_dws_task.py - 参数表:
billiards_dws.cfg_index_parameters - 结果表:
billiards_dws.dws_member_assistant_intimacy - 分位历史表:
billiards_dws.dws_index_percentile_history
执行主流程函数:IntimacyIndexTask.execute()
3. 总流程(按代码执行顺序)
- 读取门店、租户、参数
- 抽取助教服务记录(近
lookback_days) - 按
(member_id, assistant_id)分组并做“会话合并” - 做充值归因(服务结束后
recharge_attribute_hours内充值) - 计算分项分数
F/R/M/D和激增放大mult - 合成
raw_score - 把
raw_score映射到display_score(0-10) - 保存分位历史(支持 EWMA 平滑)
- 删除旧记录并写入新记录
4. 数据抽取口径
4.1 服务记录(_extract_service_records)
来源表:billiards_dwd.dwd_assistant_service_log,并 JOIN billiards_dwd.dim_assistant 获取 assistant_id。
过滤条件:
site_id = 当前门店tenant_member_id > 0(排除散客)is_delete = 0user_id > 0last_use_time在[now - lookback_days, now)内dim_assistant.scd2_is_current = 1
输出核心字段:
member_idassistant_idassistant_user_idstart_timeend_time(对应last_use_time)duration_minutes(income_seconds / 60)skill_id
5. 会话合并逻辑(_group_and_merge_sessions)
先按 (member_id, assistant_id) 分组,再对每组按 start_time 排序后做合并。
5.1 合并规则
- 相邻两条服务若满足:
next.start_time - current.session_end <= session_merge_hours(默认 4 小时) - 则视为同一次会话,执行:
session_end = max(end_time)total_duration_minutes += 当前时长course_weight = max(历史权重, 当前权重)is_incentive = 历史 or 当前
5.2 课型与权重
通过 get_course_type(skill_id) 决定课型:
BONUS:权重incentive_weight(默认 1.5)- 其他:权重 1.0
get_course_type 依赖 cfg_skill_type。若未命中映射,默认 BASE(权重 1.0)。
5.3 会话级统计
每个客户-助教对会得到:
session_counttotal_duration_minutesbasic_session_countincentive_session_countdays_since_last_session
6. 充值归因逻辑(_extract_attributed_recharges)
来源表:billiards_dwd.dwd_recharge_order
查询条件:
site_id = 当前门店member_id IN 本轮出现的会员settle_type = 5(充值订单)pay_time >= now - lookback_days
归因条件(对每笔充值):
- 找到该会员对应的会话
- 若
session_end <= pay_time且pay_time - session_end <= recharge_attribute_hours(默认 1 小时) - 则记为该助教贡献:
attributed_recharge_count += 1attributed_recharge_amount += pay_amount- 记录一条
AttributedRecharge
7. 分数计算(_calculate_component_scores)
7.1 时间衰减函数
来自 BaseIndexTask.decay(days, halflife):
decay(d, h) = exp(-ln(2) * d / h)
含义:d = h 时权重衰减到 0.5。
7.2 分项定义
设:
w_i= 会话权重(1.0 或 1.5)d_i= 会话距今天数(按session_end)A0=amount_base(默认 500)
F:频次强度
F = sum( w_i * decay(d_i, halflife_session) )
R:最近温度
R = decay(days_since_last_session, halflife_last),无最近会话则 0
M:归因充值强度
对每笔归因充值 r:
M += ln(1 + pay_amount_r / A0) * decay(days_ago_r, halflife_recharge)
D:时长贡献
D = sum( sqrt(duration_hours_i) * w_i * decay(d_i, halflife_session) )
其中 duration_hours_i = total_duration_minutes_i / 60
burst 与 mult:激增放大
先算:
F_short = sum( w_i * decay(d_i, halflife_short) )F_long = sum( w_i * decay(d_i, halflife_long) )
再算:
ratio = F_short / (F_long + 1e-6)burst = ln(1 + (ratio - 1))当ratio > 1,否则0mult = 1 + burst_gamma * burst
8. Raw Score 合成
raw_score = (weight_frequency * F + weight_recency * R + weight_recharge * M + weight_duration * D) * mult
默认权重(代码默认值):
weight_frequency = 2.0weight_recency = 1.5weight_recharge = 2.0weight_duration = 0.5burst_gamma = 0.6
9. Display Score(0-10)映射
由 BaseIndexTask.batch_normalize_to_display 完成。
- 收集全体
raw_score - 计算分位点
q_l/q_u(默认 P5/P95) - 可选 EWMA 平滑分位点(
use_smoothing=1时) - Winsorize:
clipped = min(max(raw, q_l), q_u) - 可选压缩(
compression_mode):0 -> none1 -> log1p2 -> asinh
- MinMax 映射到
[0,10] - 四舍五入到 2 位小数
特殊情况:
- 若
max_val - min_val < 1e-6,直接返回 5.0(避免分母接近 0)
EWMA 公式:
Q_t = (1 - alpha) * Q_{t-1} + alpha * Q_now,默认 alpha=0.2
10. 参数加载优先级
函数:_load_params()
- 先用代码默认参数(
DEFAULT_PARAMS) - 再用数据库参数覆盖(
cfg_index_parameters)
数据库参数加载规则(load_index_parameters):
- 只取
effective_from <= CURRENT_DATE且effective_to未过期 - 按
effective_from DESC排序 - 同名参数取第一条
即:DB > 代码默认值。
11. 持久化逻辑
函数:_save_intimacy_data
- 先删除当前门店下本轮
(member_id, assistant_id)对应旧记录 - 再逐条插入新结果
- 插入字段包含:
- 输入特征(会话数、时长、归因充值等)
- 分项得分(F/R/M/D、burst)
raw_score/display_score- 时间戳(
calc_time/created_at/updated_at)
唯一键:(site_id, member_id, assistant_id)。
12. 默认参数清单(代码 + 种子一致)
| 参数 | 默认值 | 含义 |
|---|---|---|
lookback_days |
60 | 回看窗口(天) |
session_merge_hours |
4 | 会话合并间隔(小时) |
recharge_attribute_hours |
1 | 充值归因窗口(小时) |
amount_base |
500 | 充值强度压缩基数 |
incentive_weight |
1.5 | 附加课权重 |
halflife_session |
14 | 会话衰减半衰期 |
halflife_last |
10 | 最近服务衰减半衰期 |
halflife_recharge |
21 | 充值衰减半衰期 |
halflife_short |
7 | 短期频次半衰期 |
halflife_long |
30 | 长期频次半衰期 |
weight_frequency |
2.0 | F 权重 |
weight_recency |
1.5 | R 权重 |
weight_recharge |
2.0 | M 权重 |
weight_duration |
0.5 | D 权重 |
burst_gamma |
0.6 | 激增放大系数 |
percentile_lower |
5 | 下分位(P5) |
percentile_upper |
95 | 上分位(P95) |
ewma_alpha |
0.2 | 分位平滑系数 |
compression_mode |
1 | 压缩方式(1=log1p) |
use_smoothing |
1 | 是否启用 EWMA |
13. 代码语义下的关键注意点(非常重要)
以下不是业务理想设计,而是“按当前实现”的真实行为:
- 课型映射依赖
cfg_skill_type
- 若
skill_id未映射,默认按BASE处理(不会给 1.5 权重)。
- 会话合并后权重取
max
- 同一合并会话里如果出现过
BONUS,整个会话的course_weight可能被抬到 1.5。
- 充值归因“注释意图”与“实际循环”可能有偏差
- 代码注释写“1 笔充值只归因 1 个助教”,
- 但
break只跳出“会话循环”,不会跳出“pair 循环”,在特定时序下同一笔充值可能落到多个助教对上。
- Display Score 是相对分
- 同一人不同批次跑数,若整体分布变化,即使 raw 接近,display 也可能变化(因分位映射)。
14. 一句话总结
当前亲密指数本质上是:“近期加权服务频次 + 最近接触 + 归因充值 + 服务时长” 的加权和,再乘上短期活跃激增放大因子,最后经分位截断与归一化映射到 0-10。