Commit faac3a8c by luoqi

test(recall): verify-recall §C6/§C6b — 页面事实视角的应治未治全集 vs 召回(独立验证)

用户担忧召回算法,补一层从【页面牙位事实视角】的独立验证(差分测试盲区):
现有 §C 宇宙 vt_diag 预先删了 cooldown、滤了合规闸 → 那些牙不进交叉表,但页面照常展示,
'看着该召没召'的牙被静默放过。§C6 从原始事实独立重建'应治未治'全集(口径=人眼读页面:
有结构诊断 + 本牙后续无对应治疗 + 未被更晚诊断取代),不预过滤,逐颗归因。

- §C6 牙位级:全集 vs 召回 + 未召逐层归因(合规/废用乳牙/§E/冷静期/未来预约/近期到诊/9无法解释)。
- §C6b 全口码 K05/K07:同口径(含 cooldown=30 检查)。
- 全新独立 1000 抽样(899患者,与旧样本仅重合9)验证:
  §A-§C5 全绿(FP 0/0、真·无法解释 FN 0、怪码哨兵净);
  §C6 应治未治 1762 → 已召 1476 + 全可归因 286(废用63/§E8/冷静期43/未来171/近期1)→ 9_真无法解释=0;
  §C6b K05/K07 全部已召或冷静期内 → 未召无理由=0。
  初判 1 个 K07 疑似(翟俊程)经查 = 诊断28天<30天冷静期,正确不召(非漏召;§C6b 补 cooldown 检查后归零)。
结论:召回算法与牙位事实视角零真不一致 —— 页面上每颗应治未治牙,要么已召、要么有可见的正当排除。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
parent d3b7b70b
......@@ -219,3 +219,102 @@ WHERE type='diagnosis_record' AND status='active'
(content->>'code'='K00' AND content->>'name_zh' ~ '龋|牙髓|牙周炎' AND content->>'name_zh' !~ '发育|萌出|多生|融合|先天')
)
GROUP BY 1,2 ORDER BY n DESC LIMIT 30;
-- ════════════════════════════════════════════════════════════════════════
-- §C6 页面事实视角 — 牙位"应治未治"全集(无 cooldown/合规预过滤)vs 召回,逐颗归因
-- §C 的宇宙 vt_diag 预先 DELETE 了 cooldown、JOIN 了合规闸 → 那些牙根本不进交叉表;
-- 但【页面照样展示】(tooth-timeline 零推断,只按牙位陈列诊断/治疗事实)。于是"页面看着
-- 诊断了没治疗、却没召"的牙,§C/§C3 静默不列(= 用户担忧的盲区)。
-- 本层从原始事实独立重建"应治未治"全集,口径 = 人眼读页面:
-- 有结构诊断(召回码)+ 本牙之后无对应治疗(resolver 家族)+ 未被同牙更晚诊断取代。
-- 不预过滤,逐颗标注【为何没召】。'9_真·无法解释' = 既非已召、又无任何可解释排除
-- → 召回算法与牙位事实的【真不一致】(疑似漏召 bug)。
-- ════════════════════════════════════════════════════════════════════════
CREATE TEMP TABLE vt_diag_raw AS
SELECT d.patient_id, c.code, c.sub, c.resolver, c.drop_decid, c.cooldown,
tk AS tooth, max(d.occurred_at) AS dx_at,
string_agg(DISTINCT d.content->>'name_zh','/') AS name_zh
FROM patient_facts d
JOIN vt_codes c ON c.code = d.content->>'code'
CROSS JOIN LATERAL unnest(string_to_array(regexp_replace(coalesce(d.content->>'tooth_position',''),'[^0-9;]+',';','g'),';')) tk
WHERE d.type='diagnosis_record' AND d.status='active' AND tk ~ '^[0-9]{2}$'
GROUP BY d.patient_id, c.code, c.sub, c.resolver, c.drop_decid, c.cooldown, tk;
CREATE INDEX ON vt_diag_raw(patient_id, tooth); ANALYZE vt_diag_raw;
CREATE TEMP TABLE vt_final_raw AS
SELECT d.patient_id, d.code, d.sub, d.tooth, d.dx_at, d.name_zh,
EXISTS(SELECT 1 FROM vt_treat t WHERE t.patient_id=d.patient_id AND t.tooth=d.tooth AND t.cat=ANY(d.resolver) AND t.tx_at>=d.dx_at) AS resolved,
EXISTS(SELECT 1 FROM vt_recall r WHERE r.patient_id=d.patient_id AND r.sub=d.sub AND r.tooth=d.tooth) AS recalled,
EXISTS(SELECT 1 FROM vt_struct s WHERE s.patient_id=d.patient_id AND s.tooth=d.tooth AND s.code<>d.code AND s.at>=d.dx_at) AS x_superseded,
(d.dx_at > now() - (d.cooldown||' days')::interval) AS x_cooldown,
EXISTS(SELECT 1 FROM patients p JOIN patient_profiles pp ON pp.patient_id=p.id
WHERE p.id=d.patient_id AND (NOT p.active OR pp.do_not_contact OR pp.deceased)) AS x_compliance,
(d.patient_id IN (SELECT patient_id FROM vt_future)) AS x_future,
(d.patient_id IN (SELECT patient_id FROM vt_recent)) AS x_recent,
(d.name_zh IN('废用牙','无功能牙')) AS x_inelig,
(d.drop_decid AND d.tooth ~ '^[5-8][1-5]$') AS x_decid,
(d.code='K08' AND d.tooth ~ '^[1-4]8$') AS x_thirdmolar,
(d.code='K08' AND COALESCE(d.name_zh,'') LIKE '%先天%') AS x_congenital,
(d.code='K08' AND d.patient_id IN (SELECT patient_id FROM vt_ortho_pt)
AND EXISTS(SELECT 1 FROM vt_treat t WHERE t.patient_id=d.patient_id AND t.tooth=d.tooth AND t.cat='surgical')) AS x_orthoextract
FROM vt_diag_raw d;
ANALYZE vt_final_raw;
\echo '════════ §C6 页面事实视角:牙位应治未治全集(无预过滤)vs 召回 ════════'
\echo '---- 全集规模(口径=页面人眼:有诊断 + 本牙后续无对应治疗 + 未被更晚诊断取代)----'
CREATE TEMP VIEW vt_open AS SELECT * FROM vt_final_raw WHERE NOT resolved AND NOT x_superseded;
SELECT count(*) AS 应治未治牙, count(DISTINCT patient_id) AS 患者,
count(*) FILTER (WHERE recalled) AS 已召, count(*) FILTER (WHERE NOT recalled) AS 未召 FROM vt_open;
\echo '---- 未召逐层归因(硬→软;9=无法解释=疑似漏召 bug)----'
CREATE TEMP VIEW vt_open_fn AS
SELECT *,
CASE
WHEN x_compliance THEN '1_合规闸(已故/勿扰/停用)'
WHEN x_inelig OR x_decid THEN '2_废用牙/乳牙'
WHEN x_thirdmolar OR x_congenital OR x_orthoextract THEN '3_§E剔除(智齿/先天/正畸减数)'
WHEN x_cooldown THEN '4_冷静期内(页面可见,未到召回时机)'
WHEN x_future THEN '5_未来预约⑤b'
WHEN x_recent THEN '6_近期到诊⑤f'
ELSE '9_真·无法解释(疑似漏召)'
END AS bucket
FROM vt_open WHERE NOT recalled;
SELECT bucket, count(*) AS , count(DISTINCT patient_id) AS 患者 FROM vt_open_fn GROUP BY bucket ORDER BY bucket;
\echo '---- 9_真·无法解释 明细(召回算法 vs 牙位事实的真不一致,逐颗)----'
SELECT pt.name AS 患者, f.code, f.sub AS 场景, f.tooth AS 牙位, f.dx_at::date AS 诊断日, f.name_zh AS 诊断名
FROM vt_open_fn f JOIN patients pt ON pt.id=f.patient_id
WHERE f.bucket='9_真·无法解释' ORDER BY f.code, pt.name LIMIT 50;
\echo '---- 4_冷静期内 样例(页面看着应治未治,实为还没到召回时机)----'
SELECT pt.name AS 患者, f.code, f.tooth AS 牙位, f.dx_at::date AS 诊断日,
(now()::date - f.dx_at::date) AS 诊断天数, f.name_zh AS 诊断名
FROM vt_open_fn f JOIN patients pt ON pt.id=f.patient_id
WHERE f.bucket LIKE '4_%' ORDER BY f.dx_at DESC LIMIT 12;
-- §C6b 全口码(K05 牙周 / K07 正畸)应治未治 vs 召回(无对应全口治疗晚于最新诊断)
\echo '════════ §C6b 全口码(K05牙周/K07正畸)应治未治 vs 召回 ════════'
WITH wm_dx AS (
SELECT d.patient_id, d.content->>'code' code, max(d.occurred_at) last_dx
FROM patient_facts d WHERE d.type='diagnosis_record' AND d.status='active' AND d.content->>'code' IN('K05','K07') GROUP BY 1,2),
wm_tx AS (
SELECT patient_id, content->>'category' cat, max(occurred_at) last_tx FROM patient_facts
WHERE type='treatment_record' AND kind='actual' AND status IN('active','fulfilled') AND content->>'category' IN('periodontic','orthodontic') GROUP BY 1,2),
wm_rec AS (
SELECT fp.patient_id, pr.signals->>'subKey' sub FROM plan_reasons pr JOIN followup_plans fp ON fp.id=pr.plan_id
WHERE fp.status IN('active','assigned') AND pr.signals->>'subKey' IN('perio_no_srp','ortho_no_consult') GROUP BY 1,2)
SELECT dx.code,
count(*) AS 应治未治患者,
count(*) FILTER (WHERE rec.patient_id IS NOT NULL) AS 已召,
-- K05/K07 cooldownDays 均=30:诊断 < 30 天仍在冷静期,页面可见但未到召回时机(非漏召)
count(*) FILTER (WHERE rec.patient_id IS NULL AND dx.last_dx > now() - interval '30 days') AS 冷静期内,
count(*) FILTER (WHERE rec.patient_id IS NULL AND dx.last_dx <= now() - interval '30 days'
AND NOT(NOT p.active OR pp.do_not_contact OR pp.deceased)
AND dx.patient_id NOT IN(SELECT patient_id FROM vt_future)
AND dx.patient_id NOT IN(SELECT patient_id FROM vt_recent)) AS 未召_无明显理由
FROM wm_dx dx
JOIN patients p ON p.id=dx.patient_id JOIN patient_profiles pp ON pp.patient_id=dx.patient_id
LEFT JOIN wm_tx tx ON tx.patient_id=dx.patient_id AND tx.cat=CASE dx.code WHEN 'K05' THEN 'periodontic' ELSE 'orthodontic' END AND tx.last_tx>=dx.last_dx
LEFT JOIN wm_rec rec ON rec.patient_id=dx.patient_id AND rec.sub=CASE dx.code WHEN 'K05' THEN 'perio_no_srp' ELSE 'ortho_no_consult' END
WHERE tx.patient_id IS NULL
GROUP BY dx.code;
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