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

298 lines
8.4 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 亲密指数计算说明(代码翻译版)
## 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. 总流程(按代码执行顺序)
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_score`0-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_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_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_time``pay_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. Winsorize`clipped = 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_DATE``effective_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 权重)。
2. 会话合并后权重取 `max`
- 同一合并会话里如果出现过 `BONUS`,整个会话的 `course_weight` 可能被抬到 1.5。
3. 充值归因“注释意图”与“实际循环”可能有偏差
- 代码注释写“1 笔充值只归因 1 个助教”,
-`break` 只跳出“会话循环”不会跳出“pair 循环”,在特定时序下同一笔充值可能落到多个助教对上。
4. Display Score 是相对分
- 同一人不同批次跑数,若整体分布变化,即使 raw 接近display 也可能变化(因分位映射)。
---
## 14. 一句话总结
当前亲密指数本质上是:**“近期加权服务频次 + 最近接触 + 归因充值 + 服务时长”** 的加权和,再乘上**短期活跃激增放大因子**,最后经分位截断与归一化映射到 0-10。