初始提交:飞球 ETL 系统全量代码

This commit is contained in:
Neo
2026-02-13 08:05:34 +08:00
commit 3c51f5485d
441 changed files with 117631 additions and 0 deletions

View File

@@ -0,0 +1,297 @@
# 亲密指数计算说明(代码翻译版)
## 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。