Commit 7b6cdb9f by luoqi

test(recall): verify-recall.sql 同步 gap 核心六类修复 — 新增 4 个解释桶

扫描器独立重实现召回逻辑,不补会把新排除误报成"真·无法解释 FN"。
- 拆线→surgical、ICD K00→K08:数据层改动,reparse 后扫描器自动读到,无需改 SQL;
- 固定桥/检查文本/无意愿/外院:补 vt_bridge / vt_norestor / vt_refusal / vt_external
  四张解释表,接入 §C3/§C4/§C6 归因桶(7a/7b/7c/7d、3b)。
本地 21 人跑通无错,新桶正确归类(7a 固定桥 3、7b 检查文本 3)。

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
parent cbb0750c
......@@ -89,6 +89,53 @@ CREATE TEMP TABLE vt_ortho_pt AS
UNION SELECT DISTINCT patient_id FROM patient_facts WHERE type='treatment_record' AND content->>'category'='orthodontic';
CREATE INDEX ON vt_ortho_pt(patient_id); ANALYZE vt_ortho_pt;
-- ── 新排除解释表(对齐 gap 核心 2026-06 六类修复;拆线/ICD 已随 reparse 自动正确,无需表)──
-- (2) 固定桥跨度:同一牙弓 ≥2 颗 prosthodontic actual → 基牙跨度区间内全牙位计入已修复
CREATE TEMP TABLE vt_bridge AS
WITH bt AS (
SELECT t.patient_id,
CASE substr(tk,1,1) WHEN '1' THEN 9-substr(tk,2,1)::int WHEN '2' THEN 8+substr(tk,2,1)::int
WHEN '4' THEN 9-substr(tk,2,1)::int WHEN '3' THEN 8+substr(tk,2,1)::int END AS idx,
CASE WHEN substr(tk,1,1) IN('1','2') THEN 'U' ELSE 'L' END AS arch
FROM patient_facts t
CROSS JOIN LATERAL unnest(string_to_array(regexp_replace(coalesce(t.content->>'tooth_position',''),'[^0-9;]+',';','g'),';')) tk
WHERE t.type='treatment_record' AND t.kind='actual' AND t.status IN('active','fulfilled')
AND t.content->>'category'='prosthodontic' AND tk ~ '^[1-4][1-8]$'
), span AS (SELECT patient_id, arch, min(idx) mi, max(idx) ma FROM bt GROUP BY patient_id, arch HAVING count(*)>=2)
SELECT s.patient_id,
CASE WHEN s.arch='U' THEN CASE WHEN gi<=8 THEN '1'||(9-gi)::text ELSE '2'||(gi-8)::text END
ELSE CASE WHEN gi<=8 THEN '4'||(9-gi)::text ELSE '3'||(gi-8)::text END END AS tooth
FROM span s CROSS JOIN generate_series(s.mi, s.ma) gi;
CREATE INDEX ON vt_bridge(patient_id, tooth); ANALYZE vt_bridge;
-- (3) 检查文本无修复指征:emr exam_findings 牙位 message "缺牙+间隙关闭/无修复间隙/间隙不足"
CREATE TEMP TABLE vt_norestor AS
SELECT src.patient_id, tk AS tooth
FROM (SELECT patient_id, content->>'exam_findings' AS ef_text FROM patient_facts
WHERE type='emr_record' AND status IN('active','fulfilled')
AND content->>'exam_findings' ~ '^\[' AND content->>'exam_findings' ~ '缺[牙失]'
AND content->>'exam_findings' ~ '间隙关闭|间隙已关闭|无修复间隙|间隙不足') src
CROSS JOIN LATERAL jsonb_array_elements(src.ef_text::jsonb) ef
CROSS JOIN LATERAL unnest(string_to_array(regexp_replace(coalesce(ef->>'toothPosition',''),'[^0-9;]+',';','g'),';')) tk
WHERE ef->>'message' ~ '缺[牙失]' AND ef->>'message' ~ '间隙关闭|间隙已关闭|无修复间隙|间隙不足' AND tk ~ '^[0-9]{2}$';
CREATE INDEX ON vt_norestor(patient_id, tooth); ANALYZE vt_norestor;
-- (4) 患者无意愿(按牙位):treatment subtype 含无意愿/不愿/拒绝(任意 kind)
CREATE TEMP TABLE vt_refusal AS
SELECT t.patient_id, t.content->>'category' AS cat, tk AS tooth, COALESCE(t.occurred_at,t.planned_for) AS at
FROM patient_facts t
CROSS JOIN LATERAL unnest(string_to_array(regexp_replace(coalesce(t.content->>'tooth_position',''),'[^0-9;]+',';','g'),';')) tk
WHERE t.type='treatment_record' AND t.status IN('active','fulfilled')
AND t.content->>'subtype' ~ '无意愿|不愿|拒绝' AND tk ~ '^[0-9]{2}$';
CREATE INDEX ON vt_refusal(patient_id, tooth); ANALYZE vt_refusal;
-- (5) 外院已治疗(回访记录 result,患者级 + 治疗日)
CREATE TEMP TABLE vt_external AS
SELECT DISTINCT patient_id, task_date AS at FROM patient_return_visits
WHERE result ~ '(外院|他院|别院|别的医院|其他医院|换了医院)[^,;。]{0,3}(治疗|种植|矫正|矫治|修复|根管|补牙|做了|做过)|(治疗|种植|矫正|矫治|修复|根管|补牙|做了|做过)[^,;。]{0,3}(外院|他院|别院|别的医院|其他医院)'
AND result !~ '(建议|打算|考虑|可能|拟|会去|要去|去咨询|去看|咨询了|不考虑)[^,;。]{0,8}(外院|他院|别院|别的?医院|其他医院)';
CREATE INDEX ON vt_external(patient_id); ANALYZE vt_external;
-- ── 分类(EXISTS 全打索引临时表,秒级)──
CREATE TEMP TABLE vt_final AS
SELECT d.patient_id, d.code, d.sub, d.tooth, d.dx_at, d.name_zh,
......@@ -104,7 +151,12 @@ SELECT d.patient_id, d.code, d.sub, d.tooth, d.dx_at, d.name_zh,
(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
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,
-- 2026-06 六类修复对齐(固定桥/检查文本/无意愿/外院)
EXISTS(SELECT 1 FROM vt_bridge b WHERE b.patient_id=d.patient_id AND b.tooth=d.tooth) AS x_bridge,
EXISTS(SELECT 1 FROM vt_norestor n WHERE n.patient_id=d.patient_id AND n.tooth=d.tooth) AS x_norestor,
EXISTS(SELECT 1 FROM vt_refusal rf WHERE rf.patient_id=d.patient_id AND rf.tooth=d.tooth AND rf.cat=ANY(d.resolver) AND rf.at>=d.dx_at) AS x_refusal,
EXISTS(SELECT 1 FROM vt_external xe WHERE xe.patient_id=d.patient_id AND xe.at>=d.dx_at::date) AS x_external
FROM vt_diag d;
ANALYZE vt_final;
......@@ -139,6 +191,10 @@ SELECT *,
WHEN x_inelig OR x_decid THEN '4_废用牙/乳牙'
WHEN x_thirdmolar OR x_congenital OR x_orthoextract THEN '5_§E剔除(智齿/先天/正畸减数)'
WHEN x_superseded THEN '6_同牙更晚诊断取代'
WHEN x_bridge THEN '7a_固定桥跨度修复'
WHEN x_norestor THEN '7b_检查文本无修复指征'
WHEN x_refusal THEN '7c_患者无意愿(拒绝该类)'
WHEN x_external THEN '7d_外院已治疗(回访)'
ELSE '9_真·无法解释'
END AS bucket
FROM vt_final WHERE NOT resolved AND NOT recalled;
......@@ -163,14 +219,14 @@ SELECT
FROM vt_final
WHERE NOT resolved AND NOT recalled
AND (x_future OR x_recent)
AND NOT (x_inelig OR x_decid OR x_thirdmolar OR x_congenital OR x_orthoextract OR x_superseded);
AND NOT (x_inelig OR x_decid OR x_thirdmolar OR x_congenital OR x_orthoextract OR x_superseded OR x_bridge OR x_norestor OR x_refusal OR x_external);
\echo '---- 可疑漏召样例(人审:这颗牙的诊断,该次到诊/未来预约是否真处理?)----'
SELECT code, sub, substr(patient_id::text,1,8) pid, tooth, dx_at::date, name_zh,
CASE WHEN x_future THEN '未来预约⑤b' ELSE '近期到诊⑤f' END AS 豁免来源
FROM vt_final
WHERE NOT resolved AND NOT recalled
AND (x_future OR x_recent)
AND NOT (x_inelig OR x_decid OR x_thirdmolar OR x_congenital OR x_orthoextract OR x_superseded)
AND NOT (x_inelig OR x_decid OR x_thirdmolar OR x_congenital OR x_orthoextract OR x_superseded OR x_bridge OR x_norestor OR x_refusal OR x_external)
ORDER BY code, tooth LIMIT 30;
\echo '════════ §D 全口码 治疗后复发被压(excludeIfEverTreated)════════'
......@@ -256,7 +312,11 @@ SELECT d.patient_id, d.code, d.sub, d.tooth, d.dx_at, d.name_zh,
(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
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,
EXISTS(SELECT 1 FROM vt_bridge b WHERE b.patient_id=d.patient_id AND b.tooth=d.tooth) AS x_bridge,
EXISTS(SELECT 1 FROM vt_norestor n WHERE n.patient_id=d.patient_id AND n.tooth=d.tooth) AS x_norestor,
EXISTS(SELECT 1 FROM vt_refusal rf WHERE rf.patient_id=d.patient_id AND rf.tooth=d.tooth AND rf.cat=ANY(d.resolver) AND rf.at>=d.dx_at) AS x_refusal,
EXISTS(SELECT 1 FROM vt_external xe WHERE xe.patient_id=d.patient_id AND xe.at>=d.dx_at::date) AS x_external
FROM vt_diag_raw d;
ANALYZE vt_final_raw;
......@@ -273,6 +333,7 @@ SELECT *,
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_bridge OR x_norestor OR x_refusal OR x_external THEN '3b_六类修复(桥跨度/无修复指征/无意愿/外院)'
WHEN x_cooldown THEN '4_冷静期内(页面可见,未到召回时机)'
WHEN x_future THEN '5_⚠软豁免·未来预约⑤b(患者级blanket,不证明本牙被处理)'
WHEN x_recent THEN '6_⚠软豁免·近期到诊⑤f(患者级,同上)'
......
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