Commit cd4303ff by luoqi

fix(recall): v3 优先级加诊断新鲜度衰减(老诊断止损)— 修重做时丢的 timeWindowFactor

v3 重做时丢了旧 scorer 的过晚衰减:新模型急迫性按【末诊】算、不看诊断年龄,
导致很多年前的老诊断(末诊也久)反而 urgency=紧急、分还高。而入池无时间上界(W3:缺口不自愈
仍入池),当初就靠 scorer 衰减止损 → 丢了就没人止损了。
- 加 computeFreshness(daysSince, windowDays):黄金窗内 1.0;过窗线性衰减到 2×窗=0.4,地板 0.4。
  per-病种(K08窗180/K07窗365 → 慢病衰减慢)。软衰减不硬切(同 W3:止损交 scorer+UI排序+客服自选)。
- 综合 = (急迫×0.4+价值×0.3+意愿×0.3) × 新鲜度。breakdown 加 freshness/base 可解释。
- 本地:58% gap 有衰减(池里大量老诊断);老 missing_tooth base8.5→分42,新鲜的照常高分。
- 注:PAC 无丢单数据(v3.0 靠已丢单止损),新鲜度衰减是 PAC 的止损替代。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
parent ec55b5be
......@@ -59,6 +59,10 @@ const TRUST_BASE: Record<string, number> = {
export interface PriorityInput {
/// C.2.1 急迫等级(urgency_level 特征)
urgencyLevel: 'urgent' | 'high' | 'mid' | 'low' | null;
/// 自诊断/建议至今天数(诊断新鲜度衰减用)
daysSince: number;
/// 该病种黄金窗上界(天,DiagnosisTreatmentMap.windowDays)— 新鲜度衰减锚点
windowDays: number;
/// 召回子场景 primaryCode(K08/K07/...)— 决定价值
primaryCode: string;
/// 剩余未治牙位数(种植/修复分档用;全口码为 0)
......@@ -82,7 +86,23 @@ export interface PriorityBreakdown {
rfmAdherence: number; // 意愿·RFM依从
intentBehavior: number; // 意愿·主诉行为
trustBase: number; // 意愿·信任基础
raw: number; // 综合 0-10(未 ×10)
freshness: number; // 诊断新鲜度因子 0.4-1.0(老诊断衰减)
base: number; // 三维加权(未乘新鲜度)
raw: number; // 综合 0-10(× 新鲜度后)
}
/**
* 诊断新鲜度因子(0.4-1.0)— 老诊断止损。
* 黄金窗内(daysSince ≤ windowDays):1.0(新鲜,满权重)
* 过窗:从 windowDays 线性衰减,到 2×windowDays 降至 0.4,之后地板 0.4。
* 理由:入池无时间上界(临床缺口不自愈仍入池),但"很多年前的诊断"可信度/可行动性低
* (可能已在他处处理 / 情况变化)→ 软衰减压低优先级,不硬切(同 W3 设计:止损交 scorer)。
* per-病种:K08 缺牙窗 180、K07 正畸窗 365 —— 慢病衰减得慢,合理。
*/
export function computeFreshness(daysSince: number, windowDays: number): number {
if (daysSince <= windowDays) return 1.0;
const t = Math.min(1, (daysSince - windowDays) / Math.max(1, windowDays));
return 1.0 - 0.6 * t;
}
export interface PriorityResult {
......@@ -129,8 +149,10 @@ export function calcPriority(input: PriorityInput): PriorityResult {
const urgency = input.urgencyLevel ? (URGENCY_SCORE[input.urgencyLevel] ?? 0) : 0;
const value = computeValue(input.primaryCode, input.toothCount);
const { willingness, rfmAdherence, intentBehavior, trustBase } = computeWillingness(input);
const freshness = computeFreshness(input.daysSince, input.windowDays);
const raw = urgency * 0.4 + value * 0.3 + willingness * 0.3;
const base = urgency * 0.4 + value * 0.3 + willingness * 0.3;
const raw = base * freshness; // 老诊断衰减(止损)
const score = Math.max(0, Math.min(100, Math.round(raw * 10)));
return {
......@@ -142,6 +164,8 @@ export function calcPriority(input: PriorityInput): PriorityResult {
rfmAdherence,
intentBehavior,
trustBase,
freshness: Math.round(freshness * 100) / 100,
base: Math.round(base * 100) / 100,
raw: Math.round(raw * 100) / 100,
},
};
......
......@@ -416,6 +416,8 @@ export class TreatmentInitiationRecallScenario implements PlanScenarioPlugin {
const { score, breakdown } = calcPriority({
urgencyLevel: persona?.urgencyLevel ?? null,
daysSince: r.days_since,
windowDays: rule.windowDays,
primaryCode: cfg.primaryCode,
toothCount,
rfmSegment: persona?.rfmSegment ?? null,
......
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