Files
ZQYY.FQ-ETL/docs/index/intimacy_index_code_translation.md

8.4 KiB
Raw Blame History

亲密指数计算说明(代码翻译版)

1. 目的

本文档不是“业务口头定义”,而是按当前代码真实实现翻译出来的计算逻辑,便于你做以下事情:

  • 跟业务同学对齐“现在系统到底怎么算的”
  • 排查为什么某个客户-助教分数高/低
  • 做参数调优前的影响评估

2. 代码入口与依赖

  • 任务主类:etl_billiards/tasks/dws/index/intimacy_index_task.py
  • 指数基类(衰减、分位、映射、平滑):etl_billiards/tasks/dws/index/base_index_task.py
  • 课型映射BASE/BONUSetl_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. 总流程(按代码执行顺序)

  1. 读取门店、租户、参数
  2. 抽取助教服务记录(近 lookback_days
  3. (member_id, assistant_id) 分组并做“会话合并”
  4. 做充值归因(服务结束后 recharge_attribute_hours 内充值)
  5. 计算分项分数 F/R/M/D 和激增放大 mult
  6. 合成 raw_score
  7. raw_score 映射到 display_score0-10
  8. 保存分位历史(支持 EWMA 平滑)
  9. 删除旧记录并写入新记录

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 = 0
  • user_id > 0
  • last_use_time[now - lookback_days, now)
  • dim_assistant.scd2_is_current = 1

输出核心字段:

  • member_id
  • assistant_id
  • assistant_user_id
  • start_time
  • end_time(对应 last_use_time
  • duration_minutesincome_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_count
  • total_duration_minutes
  • basic_session_count
  • incentive_session_count
  • days_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_timepay_time - session_end <= recharge_attribute_hours(默认 1 小时)
  • 则记为该助教贡献:
    • attributed_recharge_count += 1
    • attributed_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,否则 0
  • mult = 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.0
  • weight_recency = 1.5
  • weight_recharge = 2.0
  • weight_duration = 0.5
  • burst_gamma = 0.6

9. Display Score0-10映射

BaseIndexTask.batch_normalize_to_display 完成。

  1. 收集全体 raw_score
  2. 计算分位点 q_l/q_u(默认 P5/P95
  3. 可选 EWMA 平滑分位点(use_smoothing=1 时)
  4. Winsorizeclipped = min(max(raw, q_l), q_u)
  5. 可选压缩(compression_mode
    • 0 -> none
    • 1 -> log1p
    • 2 -> asinh
  6. MinMax 映射到 [0,10]
  7. 四舍五入到 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_DATEeffective_to 未过期
  • effective_from DESC 排序
  • 同名参数取第一条

即:DB > 代码默认值


11. 持久化逻辑

函数:_save_intimacy_data

  1. 先删除当前门店下本轮 (member_id, assistant_id) 对应旧记录
  2. 再逐条插入新结果
  3. 插入字段包含:
    • 输入特征(会话数、时长、归因充值等)
    • 分项得分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. 代码语义下的关键注意点(非常重要)

以下不是业务理想设计,而是“按当前实现”的真实行为:

  1. 课型映射依赖 cfg_skill_type
  • skill_id 未映射,默认按 BASE 处理(不会给 1.5 权重)。
  1. 会话合并后权重取 max
  • 同一合并会话里如果出现过 BONUS,整个会话的 course_weight 可能被抬到 1.5。
  1. 充值归因“注释意图”与“实际循环”可能有偏差
  • 代码注释写“1 笔充值只归因 1 个助教”,
  • break 只跳出“会话循环”不会跳出“pair 循环”,在特定时序下同一笔充值可能落到多个助教对上。
  1. Display Score 是相对分
  • 同一人不同批次跑数,若整体分布变化,即使 raw 接近display 也可能变化(因分位映射)。

14. 一句话总结

当前亲密指数本质上是:“近期加权服务频次 + 最近接触 + 归因充值 + 服务时长” 的加权和,再乘上短期活跃激增放大因子,最后经分位截断与归一化映射到 0-10。