初始提交:飞球 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,392 @@
# 指数算法说明(代码对齐版)
本文根据当前代码实现整理包含老客挽回指数WBI、新客转化指数NCI与关系指数RS/OS/MS/ML的计算流程、参数含义、归一化逻辑与用途说明。
如需业务版本(非代码版)说明,请另行补充。
## 0. 版本更新2026-02-08
1. 关系指数从旧 `INTIMACY` 切换为单任务 `RelationIndexTask`,统一产出 `RS/OS/MS/ML`
2. `ML` 口径调整为人工台账唯一真源(`dws_ml_manual_order_alloc`)。
3. `dwd_recharge_order` 的 last-touch 仅保留备用路径,默认关闭(`ML.source_mode=0`)。
4. `BaseIndexTask` 已支持按 `index_type` 隔离参数缓存与分位平滑历史,避免单任务串参。
## 1. 通用约定
1) **时间口径**
-`datetime.now(self.tz)` 为"当前时间"基准。
- 窗口仅回溯 **近 60 天**`lookback_days`)。
2) **天数截断**
- 所有"天数差"在参与衰减或间隔计算时,都会被截断到 `<= lookback_days`(默认 60 天)。
3) **半衰期衰减**
```
decay(d; h) = exp(-ln(2) * d / h)
```
- `d` 为距今天数,`h` 为半衰期(天)。
- `d=0` 时权重 1.0`d=h` 时权重 0.5。
4) **010 映射Raw → Display**
- 取全体 Raw 分数,计算 `P5/P95`
- 对 Raw 进行 **Winsorize** 截断到 `[P5, P95]`
- 可选压缩:`none / log1p / asinh`
- MinMax 映射到 `[0, 10]`
- 若范围过小(分母 < 1e-6直接返回 5.0。
- 最终展示分数保留两位小数。
5) **分位点平滑(可选)**
-`use_smoothing=1` 且存在历史分位点:
`Q_t = (1-α) * Q_{t-1} + α * Q_now`
- `α` 来自参数 `ewma_alpha`(默认 0.2)。
> 以上逻辑由 `BaseIndexTask` 提供。
---
## 2. 老客挽回指数WBI
### 2.0 作用/业务场景
- 识别需要重点“唤回”的老客,并给出可排序的优先级分数。
- 综合超期、降频、充值未回访与价值信号,衡量“召回紧迫度 + 价值潜力”。
- 结果通常用于运营触达/回访任务优先级与名单筛选。
### 2.1 数据来源与口径
- **到店记录**`billiards_dwd.dwd_settlement_head`
条件:`site_id``member_id > 0``settle_type=1`
`settle_type=3` 且关联 `dwd_assistant_service_log`**附加课/奖励课(BONUS)**
使用 `pay_time` 作为到店/服务结束时间(按天去重)。
- **充值记录**`billiards_dwd.dwd_recharge_order`
条件:`site_id``member_id > 0``settle_type=5`,取最近充值时间(回溯 60 天)。
- **会员档案**`billiards_dwd.dim_member.create_time`
- **储值卡余额**`billiards_dwd.dim_member_card_account.balance`
口径:现金储值卡 `card_type_id=2793249295533893`
> 计算窗口:到店历史取近 180 天recency 按 60 天封顶。
### 2.2 特征与分流
- `t_v = min(lookback_days_recency, days_since_last_visit)`
- `t_r = min(lookback_days_recency, days_since_last_recharge)`
- `t_a = min(t_v, t_r)`
- `visits_14d / visits_60d / visits_total(近 180 天)`
- `spend_30d / spend_180d`:按 `pay_amount`(实付)汇总
- 到店间隔:按天计算并封顶到 `lookback_days_recency`;同时记录间隔的“距今年龄”用于加权 CDF
- `recharge_unconsumed`:最近一次充值晚于最近一次到店(或无到店)时为 1
**分流规则:**
- STOP`t_a >= lookback_days_recency`(默认不写入;高余额例外可选)
- STOP_HIGH_BALANCE`enable_stop_high_balance_exception=1``sv_balance >= high_balance_threshold` 时,仍进入 WBI 计算
- NEW`visits_total <= new_visit_threshold`
`days_since_first_visit <= new_days_threshold`
`recharge_unconsumed=1``days_since_last_recharge <= recharge_recent_days`
- OLD非 STOP 且非 NEW
### 2.3 分项得分
**Overdue个人周期超期分**
基于加权经验 CDF
```
p = weighted_cdf(intervals, t_v, halflife_days, blend_min_samples)
overdue = p ^ alpha
```
- 加权 CDF 使用半衰期对历史间隔加权,近期间隔权重更高
- 若无历史间隔数据,`p = 0.5`
- 同时计算理想回访间隔(加权中位数),用于推算理想下次到店日期
**Drop近期降频**
```
expected14 = visits_60d * 14/60
drop = clip((expected14 - visits_14d) / (expected14 + 1), 0, 1)
```
**Recharge充值未回访压力**
```
recharge = decay(t_r; h_recharge) if recharge_unconsumed=1 else 0
```
**Value价值**
```
S_spend = ln(1 + spend_180d / M0)
S_bal = ln(1 + sv_balance / B0)
value = w_spend * S_spend + w_bal * S_bal
```
### 2.4 Raw Score
```
WBI_raw = w_over * overdue
+ w_drop * drop
+ w_re * recharge
+ w_value * value
```
**Recency suppression近访抑制**
```
suppression = sigmoid((t_v - recency_gate_days) / recency_gate_slope_days)
WBI_raw = WBI_raw * suppression
```
- Hard floor硬门槛
```
if t_v < recency_hard_floor_days: suppression = 0
```
- 默认:`recency_gate_days=14``recency_gate_slope_days=3`
- 默认:`recency_hard_floor_days=14`
-`recency_gate_slope_days <= 0` 时,退化为硬门槛:`t_v < recency_gate_days => suppression=0`
- 限制在 0 以上
### 2.5 输出表
`billiards_dws.dws_member_winback_index`
### 2.6 WBI 默认参数
| 参数 | 默认值 | 含义 |
|---|---:|---|
| `lookback_days_recency` | 60 | recency 窗口(天) |
| `visit_lookback_days` | 180 | 到店历史窗口(天) |
| `overdue_alpha` | 2.0 | 超期分幂次 |
| `overdue_weight_halflife_days` | 30 | 加权 CDF 半衰期 |
| `overdue_weight_blend_min_samples` | 8 | 加权/等权混合最小样本 |
| `h_recharge` | 7 | 充值衰减半衰期 |
| `amount_base_M0` | 300 | 消费压缩基数 |
| `balance_base_B0` | 500 | 余额压缩基数 |
| `w_over` | 2.0 | 超期分权重 |
| `w_drop` | 1.0 | 降频分权重 |
| `w_re` | 0.4 | 充值分权重 |
| `w_value` | 1.2 | 价值分权重 |
| `recency_gate_days` | 14 | 近访抑制门槛 |
| `recency_gate_slope_days` | 3 | 近访抑制斜率 |
| `recency_hard_floor_days` | 14 | 硬门槛天数 |
| `new_visit_threshold` | 2 | 新客到店次数阈值 |
| `new_days_threshold` | 30 | 新客建档天数阈值 |
---
## 3. 新客转化指数NCI
### 3.0 作用/业务场景
- 识别新客的“欢迎建联”与“转化召回”优先级。
- 兼顾首访后快速触达与二访转化窗口,避免对近期活跃新客过度打扰。
- 结果通常用于新客欢迎、转化跟进与触达节奏排序。
### 3.1 数据来源与口径
- 使用 `MemberIndexBaseTask` 的共享口径,与 WBI 完全一致(到店/充值/会员档案/余额、`t_v/t_r/t_a``visits_*``spend_*``recharge_unconsumed` 等)。
- 适用对象:仅 NEW 分群(分流规则见 2.2)。
### 3.2 关键分项
**Need转化紧迫度**
```
t2_max = 2 * t2_target_days
Need = clip((t_v - no_touch_days_new) / (t2_max - no_touch_days_new), 0, 1)
```
**Salvage可救度**
```
if t_a <= salvage_start: 1
elif t_a >= salvage_end: 0
else: (salvage_end - t_a) / (salvage_end - salvage_start)
```
**Recharge充值未回访压力** 同 WBI
**Value价值** 同 WBI权重可不同
### 3.3 Raw Score含欢迎建联与活跃抑制
新增逻辑:
- `Welcome`:仅首访/单访新客在 `welcome_window_days` 内触发,越接近当天分越高。
- `active_multiplier`若新客近14天来店次数较高且最近仍活跃则用 `active_new_penalty` 抑制转化召回分。
- `touch_multiplier``t_v` 未达到 `no_touch_days_new` 前,`Recharge/Value` 贡献按比例衰减,减少"刚来过就高分"。
```
NCI_raw = w_welcome * Welcome
+ active_multiplier * (
w_need * (Need * Salvage)
+ w_re * Recharge * touch_multiplier
+ w_value * Value * touch_multiplier
)
```
NCI 额外提供三个维度的展示分:`display_score`(总分)、`display_score_welcome`(欢迎分)、`display_score_convert`(转化分)。
### 3.4 输出表
`billiards_dws.dws_member_newconv_index`
### 3.5 NCI 默认参数
| 参数 | 默认值 | 含义 |
|---|---:|---|
| `no_touch_days_new` | 3 | 免打扰天数 |
| `t2_target_days` | 7 | 目标回访天数 |
| `salvage_start` | 30 | 可救度开始衰减天数 |
| `salvage_end` | 60 | 可救度归零天数 |
| `welcome_window_days` | 3 | 欢迎窗口(天) |
| `active_new_visit_threshold_14d` | 2 | 活跃抑制到店阈值 |
| `active_new_recency_days` | 7 | 活跃抑制近期天数 |
| `active_new_penalty` | 0.2 | 活跃抑制系数 |
| `w_welcome` | 1.0 | 欢迎分权重 |
| `w_need` | 1.6 | 紧迫度权重 |
| `w_re` | 0.8 | 充值分权重 |
| `w_value` | 1.0 | 价值分权重 |
---
## 4. 亲密指数INTIMACY
### 4.0 作用/业务场景
- 衡量客户与助教的关系强度与“近期温度”。
- 用于助教约课精力分配、客户-助教匹配与成功率预估等排序参考。
- 结果以 010 展示分提供横向比较。
### 4.1 数据来源与口径
- **服务记录**`billiards_dwd.dwd_assistant_service_log`
条件:`site_id``tenant_member_id > 0``is_delete = 0``user_id > 0`
时间口径:`last_use_time` 在近 `lookback_days` 天内
- **助教维度**`billiards_dwd.dim_assistant`
通过 `user_id` 关联获取 `assistant_id``scd2_is_current=1`
- **充值记录**`billiards_dwd.dwd_recharge_order`
条件:`settle_type = 5``pay_time >= now - lookback_days`
- 计算粒度为 `(member_id, assistant_id)`
### 4.2 会话合并
`(member_id, assistant_id)` 分组,按 `start_use_time` 排序:
- **间隔 ≤ session_merge_hours**(默认 4 小时)视为同一会话
- 合并后:
- `session_end` 取最大结束时间
- `duration` 累加
- `course_weight` 取最大值(附加课权重更高)
- `is_incentive` 取 OR
- 课型权重:
- `BONUS``incentive_weight`(默认 1.5
- 其他:权重 1.0
### 4.3 归因充值
`dwd_recharge_order` 取近 `lookback_days` 天充值记录(`settle_type=5`
- 若充值发生在某会话结束后的 **recharge_attribute_hours**(默认 1 小时)内,归因为该助教
- 单笔充值在该 `(member_id, assistant_id)` 对内只计一次
### 4.4 分项得分
所有 `days_ago` 均截断到 `<= lookback_days`
**F频次强度**
```
F = Σ( τ_i * decay(days_ago_i; h_sess) )
```
**R最近温度**
```
R = decay(min(d_last, lookback_days); h_last)
```
**M归因充值强度**
```
M = Σ( ln(1 + amt/A0) * decay(min(days_ago, lookback_days); h_pay) )
```
**D时长贡献**
```
D = Σ( sqrt(dur_hours) * τ_i * decay(days_ago_i; h_sess) )
```
**burst频率激增放大**
```
F_short = Σ( τ_i * decay(days_ago_i; h_short) )
F_long = Σ( τ_i * decay(days_ago_i; h_long) )
ratio = F_short / (F_long + 1e-6)
burst = max(0, ln(1 + (ratio - 1)))
mult = 1 + γ * burst
```
### 4.5 Raw Score 与 Display Score
```
INTIMACY_raw = (w_F*F + w_R*R + w_M*M + w_D*D) * mult
```
Display Score 使用 `BaseIndexTask` 的分位截断 + 压缩 + MinMax 映射到 010并可选 EWMA 平滑(见第 1/5 节通用说明)。
### 4.6 输出字段
写入表:`billiards_dws.dws_member_assistant_intimacy`,主要字段:
- 会话统计:`session_count``total_duration_minutes``basic_session_count``incentive_session_count`
- 最近与充值:`days_since_last_session``attributed_recharge_count``attributed_recharge_amount`
- 分项得分:`score_frequency``score_recency``score_recharge``score_duration``burst_multiplier`
- 汇总:`raw_score``display_score`
### 4.7 INTIMACY 默认参数
| 参数 | 默认值 | 含义 |
|---|---:|---|
| `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 |
| `compression_mode` | 1 | 压缩方式1=log1p |
| `use_smoothing` | 1 | 是否启用 EWMA |
| `ewma_alpha` | 0.2 | 分位平滑系数 |
---
## 5. 映射与参数配置
### 5.1 映射流程
1) 计算 Raw Score
2) 计算 P5/P95
3) Winsorize 截断
4) 可选压缩(`none/log1p/asinh`
5) MinMax → `[0, 10]`
6) 可选 EWMA 平滑(`use_smoothing` + `ewma_alpha`
### 5.2 参数来源
参数来自 `billiards_dws.cfg_index_parameters`,按 `index_type` 加载,默认值见代码:
- **WBI 关键参数**`lookback_days_recency``overdue_alpha``overdue_weight_halflife_days``h_recharge``w_over/w_drop/w_re/w_value`
- **NCI 关键参数**`no_touch_days_new``t2_target_days``salvage_start/end``w_welcome/w_need/w_re/w_value`
- **INTIMACY 关键参数**`halflife_session/last/recharge/short/long``amount_base``incentive_weight``weight_*``burst_gamma`
- **通用参数**`percentile_lower/upper``compression_mode``use_smoothing``ewma_alpha`
`compression_mode` 取值:
- `0`:不压缩
- `1`log1p
- `2`asinh
### 5.3 参数优先级
- 先用代码默认参数(`DEFAULT_PARAMS`
- 再用数据库参数覆盖(`cfg_index_parameters`,取 `effective_from <= CURRENT_DATE` 且未过期,同名参数取最近生效的一条)
- GUI/环境变量可通过 `run.index_lookback_days` 覆盖 recency 窗口
即:**GUI/环境变量 > DB > 代码默认值**。
---
## 6. 运行与覆盖策略
- WBI/NCI默认"每 2 小时"计算(由任务描述定义)
- INTIMACY默认"每 4 小时"计算(由任务描述定义)
- 写入方式:对本次参与计算的实体进行 **delete-before-insert** 覆盖写入
(不在窗口内的实体不会被重算)