Commit 6e68bae1 by luoqi

docs(algorithm): 通俗算法文档每个论断加 📐 证据(公式/数值/谓词)

按需求:正文保持大白话,关键论断下加简短证据块,专业人员可核,非技术可跳过。

召回算法:
- 10 情况表加 诊断码 + 基线分 + 冷静期(SUB_SCENARIOS.base / DiagnosisTreatmentMap.cooldownDays)
- 触发条件谓词(双信号源 + confidence)
- 3 道过滤网各加证据:合规 SQL 三条件 / 只设下界不设上界 / NOT EXISTS 牙位 overlap + 时间方向
- 合并规则:union-find 牙位重叠 + sub_key 标识示例

画像算法 4 标签全加阈值证据:
- 价值:净额公式 + 5 档(¥30k/10k/3k/500)
- 流失风险:540+gap/360|gap/180 天分档
- 治疗链:in_progress/closed/gap 判据
- 不打扰:DNC 硬命中条件(+phone 缺失不算 DNC)

优先级算法:
- 6 因素表 举例列 → 精确取值证据(各 bonus 分档 + timeWindow 曲线 + confidence 阶梯)
- 完整公式 priority-scorer.ts 1:1
- 卜晓平 87 分例子修正自洽:流失风险 低→中(formula (3−2)×2=+2),
  补"对应公式"列,(60×1.0+20+2+5)×1.0=87 可逐项核

(原例子"风险低 +2"跟 formula 矛盾;low→+4,medium→+2;改 medium 使总分仍 87 且自洽)
parent c2f19278
...@@ -2,7 +2,8 @@ ...@@ -2,7 +2,8 @@
> **谁该读**:产品 / 运营 / 诊所管理者 / 客服主管 —— 不需要懂技术。 > **谁该读**:产品 / 运营 / 诊所管理者 / 客服主管 —— 不需要懂技术。
> **本文目的**:用大白话讲清楚 PAC 到底在算什么、为什么这么算。 > **本文目的**:用大白话讲清楚 PAC 到底在算什么、为什么这么算。
> **不含**:代码、SQL、字段名。要看实现细节去 `potential-treatment-recall-flow.md`。 > **读法**:正文是大白话;每个关键论断下面带一个 `📐 证据` 块(简短公式 / 数值 / 判断条件),
> 专业人员看证据即可核对,非技术读者可跳过。完整实现见 `potential-treatment-recall-flow.md`。
--- ---
...@@ -11,7 +12,7 @@ ...@@ -11,7 +12,7 @@
一句话:**PAC 帮诊所找出"该回来但还没回来"的患者,告诉客服先打给谁、怎么打。** 一句话:**PAC 帮诊所找出"该回来但还没回来"的患者,告诉客服先打给谁、怎么打。**
诊所每天产生大量就诊记录(诊断、治疗、收费、预约……),但里面藏着很多"漏掉的机会": 诊所每天产生大量就诊记录(诊断、治疗、收费、预约……),但里面藏着很多"漏掉的机会":
医生说过"这颗牙该补",患者却一直没来补;种植做完该年度复查,患者忘了…… 医生说过"这颗牙该补",患者却一直没来补;种植做完该年度复查,患者忘了(将来实现)……
PAC 做三件事,像一条流水线: PAC 做三件事,像一条流水线:
...@@ -43,22 +44,34 @@ PAC 做三件事,像一条流水线: ...@@ -43,22 +44,34 @@ PAC 做三件事,像一条流水线:
- 医生说"有牙周炎,要做基础治疗",患者没做 → **该召回** - 医生说"有牙周炎,要做基础治疗",患者没做 → **该召回**
- 医生发现龋齿要补,患者没补 → **该召回** - 医生发现龋齿要补,患者没补 → **该召回**
> 📐 **证据 · 触发条件**(两个信号源,任一命中):
> ```
> 有一条 active 的「诊断记录」或「医生建议」 (diagnosis_record / recommendation_record)
> 且其 code ∈ 本子场景的诊断码/建议码集合
> 且【不存在】诊断之后启动的同类治疗(见第 3 道过滤网)
> ```
> 诊断 confidence=1.0;AI 从文本抽的建议 confidence=0.8(影响优先级,见算法三⑥)。
### 覆盖哪些情况 ### 覆盖哪些情况
按牙科疾病大类,PAC 覆盖 **10 种**"发现了没去做"的情况: 按牙科疾病大类(ICD-10 的 K00–K09),PAC 覆盖 **10 种**"发现了没去做"的情况。
表里的 **基线分** 就是这种病的"重要程度起步分"(越值钱/越严重越高,直接进优先级算法):
| 情况 | 大白话 |
|---|---| | 诊断码 | 情况 | 大白话 | 基线分 | 冷静期(天) |
| 缺失牙未修复 | 缺了牙没去种/没镶 | |---|---|---|---|---|
| 错颌畸形未正畸 | 牙不齐没去矫正 | | K08 | 缺失牙未修复 | 缺了牙没去种/没镶 | **60** | 30 |
| 牙髓炎未做根管 | 牙神经发炎没去做根管 | | K07 | 错颌畸形未正畸 | 牙不齐没去矫正 | 55 | 30 |
| 牙周炎未做基础治疗 | 牙周病没去洗治 | | K04 | 牙髓炎未做根管 | 牙神经发炎没去做根管 | 52 | 14 |
| 龋齿未充填 | 蛀牙没去补 | | K05 | 牙周炎未做基础治疗 | 牙周病没去洗治 | 50 | 30 |
| 牙体损伤未修复 | 牙磨损/缺损没去修 | | K09 | 颌骨囊肿未处理 | 颌骨囊肿没去摘 | 50 | 14 |
| 牙龈牙槽问题未处置 | 牙龈问题没去看 | | K02 | 龋齿未充填 | 蛀牙没去补 | 45 | 14 |
| 阻生牙未拔除 | 智齿该拔没拔 | | K03 | 牙体损伤未修复 | 牙磨损/缺损没去修 | 35 | 14 |
| 颌骨囊肿未处理 | 颌骨囊肿没去摘 | | K06 | 牙龈牙槽问题未处置 | 牙龈问题没去看 | 35 | 14 |
| 牙发育/萌出异常未处置 | 儿童换牙/长牙问题没去看 | | K01 | 阻生牙未拔除 | 智齿该拔没拔 | 30 | 14 |
| K00 | 牙发育/萌出异常未处置 | 儿童换牙/长牙问题没去看 | 25 | 30 |
> 📐 **证据**:基线分 = scenario 配置 `SUB_SCENARIOS[*].base`;冷静期 = `DiagnosisTreatmentMap[K??].cooldownDays`。
> 排序体现临床价值:种植(¥1.5–3 万)> 正畸 > 根管 > 牙周 > 龋齿 > 智齿。**不漏码**:出现的 K-code 都进池,低基线的靠分数自然沉底。
### 三道"过滤网"(避免打错电话) ### 三道"过滤网"(避免打错电话)
...@@ -67,11 +80,16 @@ PAC 做三件事,像一条流水线: ...@@ -67,11 +80,16 @@ PAC 做三件事,像一条流水线:
**第 1 道 · 合规红线**(硬规矩,一票否决): **第 1 道 · 合规红线**(硬规矩,一票否决):
- 已故患者 → 不打 - 已故患者 → 不打
- 标了"勿打扰"的 → 不打 - 标了"勿打扰"的 → 不打
- 没有电话的 → 没法打 - 没有电话的 → 提示客服补,但不算合规拒绝(数据问题 ≠ 不让打)
> 📐 **证据**:`p.active = true AND pp.do_not_contact = false AND pp.deceased = false`(SQL 三条 WHERE,任一不满足直接踢)。
**第 2 道 · 时间窗**(不能太早,但不嫌晚):
- **太早不打**:医生刚诊断完(比如缺牙 30 天内),患者还在考虑,这时候打像催债。每种病有自己的"冷静期"(见上表)。
- **越晚越该打**:缺牙拖了一年,比拖三个月更该召回。所以**没有"太晚就不打"这一说**——拖得久的不会被踢掉,只是在排队时往后稍微挪(交给优先级算法的时间窗因子衰减)。
**第 2 道 · 时间窗**(不能太早也不嫌太晚): > 📐 **证据**:只设下界 `诊断日 <= now − cooldownDays`,**不设上界**。
- **太早不打**:医生刚诊断完(比如缺牙 30 天内),患者还在考虑,这时候打像催债。每种病有自己的"冷静期"。 > (旧版曾有上界 = 黄金窗末,导致缺牙 >180 天被错误踢出池;W3 末已废上界。)
- **越晚越该打**:缺牙拖了一年,比拖三个月更该召回。所以**没有"太晚就不打"这一说**——拖得久的不会被踢掉,只是在排队时往后稍微挪一点(交给优先级算法处理)。
**第 3 道 · 已经做了就别打**(关键): **第 3 道 · 已经做了就别打**(关键):
- 患者**已经做了**对应治疗 → 不召回(已经补过这颗牙了,别再喊人来补) - 患者**已经做了**对应治疗 → 不召回(已经补过这颗牙了,别再喊人来补)
...@@ -79,6 +97,14 @@ PAC 做三件事,像一条流水线: ...@@ -79,6 +97,14 @@ PAC 做三件事,像一条流水线:
- 已经把这颗牙**拔了** → 这件事终结,不召回 - 已经把这颗牙**拔了** → 这件事终结,不召回
- 已经**约了**未来的号 → 人在来的路上了,不打扰 - 已经**约了**未来的号 → 人在来的路上了,不打扰
> 📐 **证据**:`NOT EXISTS(诊断之后启动的同类 actual 治疗)`,且:
> ```
> 双方都有牙位 → 牙位集合有交集才算"已做"(36 做了不挡 46)
> 信号无牙位(如牙周全口) → 按类别(category)算
> 治疗无牙位(如全口洁治) → 视为覆盖该类全部牙位
> 时间方向:治疗.occurred_at >= 诊断.occurred_at(只算诊断后启动的,历史旧治疗不算)
> ```
### 一个聪明的合并 ### 一个聪明的合并
同一个患者,可能同时有好几颗牙、好几个问题。PAC 会**智能合并**: 同一个患者,可能同时有好几颗牙、好几个问题。PAC 会**智能合并**:
...@@ -87,6 +113,16 @@ PAC 做三件事,像一条流水线: ...@@ -87,6 +113,16 @@ PAC 做三件事,像一条流水线:
这样客服看到的是**整理好的几件事**,而不是一堆零散记录。 这样客服看到的是**整理好的几件事**,而不是一堆零散记录。
> 📐 **证据 · 合并规则**(按牙位重叠做并查集 union-find):
> ```
> 同患者同病种的多条诊断:牙位集合有交集 → 并入同一组
> 每组产出一条理由,标识 = 病种@牙位:
> caries_no_filling@36 (单颗)
> impacted_tooth@18;28;38;48 (多颗合并)
> perio_no_srp@whole (全口诊断无具体牙位)
> 组内取最早诊断日(最有召回价值);两种信号源都有 → 标"(诊断+医生建议)"
> ```
> **本质**:召回算法不"猜",它只认医生白纸黑字写下的诊断和建议。医生没说的,它绝不编。 > **本质**:召回算法不"猜",它只认医生白纸黑字写下的诊断和建议。医生没说的,它绝不编。
--- ---
...@@ -98,23 +134,47 @@ PAC 做三件事,像一条流水线: ...@@ -98,23 +134,47 @@ PAC 做三件事,像一条流水线:
画像算法给每个患者打 **4 个标签**: 画像算法给每个患者打 **4 个标签**:
### 标签 1 · 患者价值 ### 标签 1 · 患者价值
**这个患者对诊所贡献多大?** 看历史累计消费、做过的治疗等。 **这个患者对诊所贡献多大?** 按累计净消费分 5 档。
- 高价值(比如累计消费几万的种植客户)→ 值得优先维护
- 普通价值 → 正常对待 > 📐 **证据**:`累计净额 = Σ收款 + Σ充值 − Σ退费`,分档:
> ```
> ≥ ¥30,000 → 钻卡 (score 4) ≥ ¥10,000 → 金卡 (3)
> ≥ ¥3,000 → 银卡 (2) ≥ ¥500 → 普通付费 (1)
> < ¥500 → 新客/未消费 (0)
> ```
> 这个 score 直接喂优先级算法的"价值加分"(见算法三③)。
### 标签 2 · 流失风险 ### 标签 2 · 流失风险
**这个患者是不是快"跑"了?** 看多久没来、该来的复查有没有断。 **这个患者是不是快"跑"了?** 看多久没来 + 治疗链有没有断口。
- 高风险(很久没来 + 有未闭环的治疗)→ 再不召回可能就永久流失了
- 低风险(还在正常往来)→ 没那么急 > 📐 **证据**(距上次临床事件天数 + 链缺口):
> ```
> ≥540 天 且 链有缺口 → high (score 3)
> ≥360 天 或 链有缺口 → medium (2)
> ≥180 天 → low (1)
> 否则 → none (0)
> ```
> 风险越低 → 越可能接电话愿意来 → 优先级"转化加分"越高(见算法三④,有点反直觉:不是越高风险越优先,而是越可能转化越优先)。
### 标签 3 · 治疗链状态 ### 标签 3 · 治疗链状态
**这个患者的治疗进行到哪一步了?** 一段治疗从"发现问题"到"做完闭环"有好几个阶段。 **这个患者的治疗进行到哪一步了?** 从"发现"到"闭环"分几个状态。
- 比如"种植已发现但没启动" / "根管做了但冠还没戴" / "已闭环"
- 让客服一眼看出"卡在哪一步" > 📐 **证据**(读独立的诊断/治疗记录判断):
> ```
> 有 actual 治疗在管(active) → in_progress(进行中)
> 所有诊断都有对应已完成治疗 → closed(已闭环)
> 有诊断但缺对应治疗 → gap(有缺口,= 召回切入点)
> ```
### 标签 4 · 不打扰状态 ### 标签 4 · 不打扰状态
**这个患者能不能联系?** 标记勿打扰、已故等合规状态。 **这个患者能不能联系?** 标记勿打扰、已故、投诉等合规状态。
- 跟召回算法的合规红线呼应,双保险
> 📐 **证据**(硬命中任一即标 DNC):
> ```
> profile.do_not_contact = true 或 profile.deceased = true 或 有投诉记录
> ⚠️ phone 缺失【不算】DNC(那是数据问题,只提示客服补,不拦召回)
> ```
> 跟召回算法第 1 道合规红线呼应,双保险。
### 两个特点 ### 两个特点
...@@ -133,32 +193,34 @@ PAC 做三件事,像一条流水线: ...@@ -133,32 +193,34 @@ PAC 做三件事,像一条流水线:
把它想成**给每个召回机会打分,像考试加权**: 把它想成**给每个召回机会打分,像考试加权**:
| 因素 | 通俗解释 | 举例 | | 因素 | 通俗解释 | 📐 取值证据 |
|---|---|---| |---|---|---|
| **① 临床基线** | 这种病本身多重要(种植 > 补牙 > 拔智齿) | 缺牙起步 60 分,智齿起步 30 分 | | **① 临床基线** | 这种病本身多重要 | 病种基线分(K08=60 / K07=55 / K04=52 / K05=50 / K02=45 / … / K00=25)|
| **② 时间窗形状** | 现在是不是召回的"最佳时机" | 正当时 ×1.0;太久了打个折 | | **② 时间窗形状** | 现在是不是召回最佳时机 | 黄金窗内 ×1.0;过早 0.6→1.0 线性;过晚 1.0→0.4 衰减(2× 窗末降到底) |
| **③ 患者价值加分** | 高价值患者加分 | 钻卡客户 +20 | | **③ 患者价值加分** | 高价值患者加分 | 钻卡 +20 / 金卡 +15 / 银卡 +10 / 普通 +5 / 新客 +0 |
| **④ 转化可能加分** | 这人接电话后真来的可能性 | 流失风险低、以前召回成功过 → 加分 | | **④ 转化可能加分** | 接电话后真来的可能性 | `(3−风险档)×2`(0–6)+ 近期回访成功 +4,上限 +10 |
| **⑤ 临床紧迫加分** | 再拖会出大问题的加分 | 缺牙太久邻牙要倒 +5 | | **⑤ 临床紧迫加分** | 再拖会出大问题 | 诊断天数 > 该病种紧迫临界 → +5(如缺牙 >120 天) |
| **⑥ 信号可信度** | 这个召回理由有多靠谱 | 医生明确诊断 ×1.0;AI 从文本里猜的打个折 | | **⑥ 信号可信度** | 召回理由多靠谱 | 最低 confidence ≥0.9→×1.0;≥0.7→×0.9;否则 ×0.75 |
**算法**(不用记,理解思路就行): **完整公式**(代码 `priority-scorer.ts` 1:1):
``` ```
分数 = (临床基线 × 时间窗 + 价值加分 + 转化加分 + 紧迫加分) × 信号可信度 raw = (base × timeWindowFactor + valueBonus + likelihoodBonus + urgencyBonus) × confidenceFactor
然后压到 0~100 之间 score = clamp(round(raw), 0, 100)
``` ```
> 📐 阈值都集中在 `priority-scorer.ts` 的 `computeXxx()` 函数,业务方改分档改这一处即可。
### 真实例子:卜晓平,87 分 ### 真实例子:卜晓平,87 分
| 因素 | 他的情况 | 贡献 | | 因素 | 他的情况 | 贡献 | 对应公式 |
|---|---|---| |---|---|---|---|
| 临床基线 | 缺失牙 | 60 | | 临床基线 | 缺失牙 K08 | 60 | base |
| 时间窗 | 缺了 151 天,正当召回期 | ×1.0 | | 时间窗 | 缺了 151 天[30,180] 黄金窗 | ×1.0 | timeWindowFactor |
| 患者价值 | 钻石卡,累计消费 5.7 万+ | +20 | | 患者价值 | 钻石卡,累计 5.7 万+(≥¥3 万)| +20 | valueBonus |
| 转化可能 | 流失风险低 | +2 | | 转化可能 | 流失风险=中(链有缺口)→ (3−2)×2 | +2 | likelihoodBonus |
| 临床紧迫 | 缺太久邻牙有倾斜风险 | +5 | | 临床紧迫 | 151 > 紧迫临界 120(邻牙倾斜)| +5 | urgencyBonus |
| 信号可信度 | 医生明确诊断 | ×1.0 | | 信号可信度 | 医生明确诊断 confidence=1.0 | ×1.0 | confidenceFactor |
| **最终** | | **87 分** → 排很前面 | | **最终** | (60×1.0 + 20 + 2 + 5) × 1.0 | **= 87 分** | clamp(round(raw),0,100) |
### 为什么要"可解释" ### 为什么要"可解释"
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment