Files
CADx-4/research-wiki/guides/装配两个库.md
T
iomgaa bceb01e8fa feat: 装配两个库(不含 CEUS 结果)
按任务书 01 实现 delivery、library、answers、runs(快照部分)、build 五个模块,
scripts/build.py、scripts/scan_library.py 与 config/build.yaml(decode_codes: false)。
交付包整表检查 T1–T4、映射表检查 T5、逐例检查 C01–C13,一例遇到第一项不过即整例跳过。

与任务书原文的出入:
- C04 报告表名随视频名配对(负责人 2026-10-16 定)。
- 按两轮独立审阅加严 C11(更多换行符、比例号冒号)、C12(同义词与词根)、
  C13(空格、周岁、中文数字、繁体);泄露扫描做 NFKC;关键图只接受能保真重编码的 PNG;
  YAML 配置拒绝重复键和空路径。
- import-linter 第 1 条拆成互不依赖与不引用其他 cadx 模块两条。

文档:结构.md 第五、六、八部分按实际更新;新增 guides/装配两个库.md;WORK_LOG 更新。

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-16 03:00:20 -04:00

21 KiB
Raw Blame History

装配两个库

读者:要跑一次「装配两个库」、或者要把跳过清单交给医生的人。这一步读入医院的交付包,逐例检查,把检查全部通过的病例写成两个库:给 AI 专家看的资料库(library/)和只给评价用的标准答案库(answers/)。它在整条流水线里的位置见 ../explanation/结构.md 第二部分;为什么这样设计见 ../任务书/01-装配两个库.md 和 ../design/0002-四段流水线与两个库.md。

先认识几个词

  • 四类胰腺囊性肿瘤:IPMN(导管内乳头状黏液性肿瘤)、SPN(实性假乳头状肿瘤)、MCN(黏液性囊性肿瘤)、SCN(浆液性囊性肿瘤)。项目要专家判断病灶属于哪一类,所以资料库里出现这些名字就等于泄露答案。
  • 金标准:术后或穿刺的病理结论,交付表里叫「病理类型」或 Diagnosis,用 1–4 的数字记。
  • CEUS、MR:CEUS 是超声造影视频(往静脉打造影剂后录的超声);MR 是磁共振检查,本项目只有它的文字报告。这一步不处理 CEUS 视频,只检查它在不在。
  • 交付包:临床方交来的一批原始资料,目前是 data/交付包_20260908_40例/:三张汇总表(临床工作簿、超声报告、MR 报告)和每例一个病例目录(关键图、CEUS 视频、单例报告表)。
  • 单例报告表:每个病例目录里的一份 Excel,只有一行,列与超声汇总表相同(7 列)。它和汇总表重复,用来核对两份是否一致。
  • 所见、结论段:一份影像报告通常分「所见」(描述看到了什么)和「印象 / 结论」(医生的诊断倾向)。交付包的报告文字只有所见,这一步检查里面没有混进结论段(C11)。
  • roi2:交付视频文件名里的版本标记,表示视频上带医生画的病灶框(ROI,感兴趣区)。
  • 推注开始时间:超声表的一列,单位秒,表示视频第 0 秒时造影剂已经推注(注射)了多少秒。以后 CEUS 定量(从视频里算亮度曲线、挑关键帧、算灌注指标的那一步)要用,这一步只检查它是非负整数,不放进资料库。
  • 档位:临床表每项化验除原值外还有一个 *_Level 列,取 0/1,看起来是正常/异常,未经临床方确认。
  • 三路:临床、超声、MR。每个 AI 专家只看自己那一路(AGENTS.md 硬性约束 2)。
  • 构建:跑一次 scripts/build.py,产出一对同号的 library/<构建号>/ 和 answers/<构建号>/。构建完不再改,要改就重新构建。
  • 化名:资料库里代替病例号的 4 位随机名字(小写字母加数字;36 的 4 次方约 168 万种,几十例足够,抽到重复的或含亚型缩写的就重抽)。真实病例号会暴露交付顺序,而交付表是按诊断分组录入的,顺序本身带着答案(AGENTS.md 硬性约束 1)。
  • 编码列:交付包里用数字表示类别的列,如性别 1/2、症状 0/1。数字代表什么交付包没写,要向临床方要映射表。
  • 结论段:报告里医生写诊断倾向的那一段(「印象:」「提示:」之类),接近答案,不能给专家。
  • NFKC:Unicode 的一种归一化,把全角字母、数字、标点转成半角。文字检查都先做它,全角写法就绕不过去。

一、改配置

配置是 config/build.yaml,进 git。字段定义和校验在 src/cadx/build/_config.py 的 BuildConfig:每个字段必填,没有默认值;多写一个字段、拼错字段名、同一层出现重复的键都直接报错;除路径以外不做类型转换(例如 alias_seed 写成 "7" 会报错)。这样配置写错时构建停在第一步,而不是用一个没人知道的值跑完(AGENTS.md「不设默认值」)。

字段 类型 含义
delivery_dir 路径 交付包目录
library_root 路径 资料库的上级目录,每次构建在它下面新建 <构建号>/
answers_root 路径 答案库的上级目录,同上
alias_seed 整数 生成化名的随机种子。同一个种子、同一批通过的病例,得到同样的化名
decode_codes true / false 编码列是否按映射表解码
code_maps 路径或 null 映射表位置

相对路径相对仓库根目录解析,不相对配置文件所在目录,也不相对你敲命令时所在的目录。路径不能写空串(空串会变成当前目录)。

decode_codes 与 code_maps 要配套

  • decode_codes: false 时 code_maps 必须是 null。编码列按原始数字写进资料库,clinical.json 里 codes_decoded 为 false;答案库 diagnoses.csv 的「诊断名」一栏留空;临床表的性别(1/2)没法和报告表的「男」「女」比对,资料库清单的 uncompared 里写明「临床性别未比对」。
  • decode_codes: true 时 code_maps 必须是映射表的路径。
  • 配错(true 配 null,或 false 配路径)读配置时就报错。

为什么做成开关:映射表还没从临床方拿到,不能编造含义;先用 false 跑通,映射表到位后改成 true 重新构建一次(负责人 2026-10-16:解码做成可选开关,映射表是独立的配置文件)。诊断编码和其他编码列一样归这个开关管;诊断不进资料库,所以开关对诊断只影响答案库。

映射表的格式

映射表目前还没有(config/code_maps.yaml 尚未建)。拿到后按下面的格式写:每个编码列一项,列名映射到「数字 → 含义」。

Sex: {1: ..., 2: ...}
Symptom_Pain: {0: ..., 1: ...}
  • 键必须是整数(不加引号),含义必须是字符串。YAML 会把不加引号的 yes、no、on、off 读成布尔值,含义一律加引号最稳;读成布尔值时会报错,不会悄悄通过。
  • 需要映射的 17 项列名是 src/cadx/delivery/_shape.py 里的 CODED_COLUMNS(性别、诊断、5 个症状、5 项既往史、5 项化验的档位列)。其中 Diagnosis 一项同时管临床表的 Diagnosis 列和两张报告表的「病理类型」列,三列编码相同。名单放在 delivery,是因为「哪些列是编码列」是交付包形状的一部分。
  • 映射表在构建开始时检查(检查编号 T5,代码在 src/cadx/build/_config.py 的 load_code_maps):必须恰好覆盖这 17 项,多一项少一项都整次构建停下;Sex 的含义必须恰好是「男」「女」,才能和报告表的性别比对;Diagnosis 的含义必须恰好是 IPMN、SPN、MCN、SCN,评价要拿它和专家的结论比。
  • 解码时,某一例出现映射表里没有的值,这一例按 C07(临床表)或 C08、C09(报告表的诊断)跳过。

二、怎么跑

uv run python scripts/build.py --config config/build.yaml

脚本只接收 --config 一个参数,不能在命令行单独改某个参数,这次用了什么参数只看配置文件就知道(结构.md 第五部分「配置」)。

终端只打印计数:构建号、两个库的路径、病例总数、进库病例数、跳过病例数、认不出编号的行数、按检查编号分的跳过数、复核清单条数、跳过清单的说明句。不打印病例号和任何资料内容,终端输出可以贴进汇报。

跑完建议再做一次泄露扫描:

uv run python scripts/scan_library.py --library library/<构建号> --answers answers/<构建号>

它从答案库读出这次构建的真实病例号和视频号,加上脚本里独立写的亚型名、诊断列名、交付文件名词表,扫资料库的全部 .txt、.json(不扫 PNG:PNG 的附加数据在写入时已经去掉,像素上烧录的信息栏按 AGENTS.md 硬性约束 3 不遮蔽,文字扫描也查不了像素),并检查 JSON 里有没有 diagnosis、label、gold、pathology、name、row 这类键名。只输出命中的条数和类别,不输出命中的内容;全部为 0 才说明资料库里没有这些东西。两个目录的构建号不同时直接报错。词表在脚本里另写一份、不取自 delivery 的常量,是为了让扫描不依赖被检查的代码本身:delivery 漏了一个词,扫描还能查出来。这个脚本同时读两个库,所以只能放在 scripts/,不能进 src/cadx(会诊代码不许碰答案库)。

三、产物在哪

构建号

YYYYMMDD-HHMMSS-<git 短哈希>,时间是本地时间(代码在 src/cadx/runs/_snapshot.py 的 make_build_id)。工作区有未提交的改动(含未跟踪的文件)时末尾加 -dirty。不干净的工作区照样允许构建,但如实记下:带 -dirty 的构建,代码不能只凭提交号复现,快照里另记了 git diff HEAD 输出的哈希。要写进论文的构建,应在干净的工作区上跑。

资料库 library/<构建号>/

manifest.json
cases/<化名>/
  clinical/clinical.json
  ultrasound/report.txt
  ultrasound/key.png
  mr/report.txt
文件 内容
manifest.json 构建号、git 提交号、工作区是否干净、配置文件(解码时含映射表)的 SHA-256、codes_decoded、uncompared(没比对的项)、ceus_source(所用 CEUS 结果的来源,这一步是 null)、化名列表(按化名排序)、每个文件的相对路径和 SHA-256。不含真实病例号、交付文件名和诊断
clinical.json {"codes_decoded": ..., "fields": {列名: 值}}。字段是临床表去掉编号和诊断后的 22 列:性别、年龄、5 个症状、5 项既往史、5 项化验的原值和档位。没测的化验写 null
ultrasound/report.txt 超声报告的「所见」原文,UTF-8,不加标题
ultrasound/key.png 医生截的超声关键图,只保留像素和颜色模式重新编码,不带文本块、EXIF、ICC 等附加数据(附加数据里可能有文件名、时间等线索)。超声帧上烧录的信息栏不遮蔽(AGENTS.md 硬性约束 3)
mr/report.txt MR 报告的「所见」原文

性别、年龄只在临床一路:两份报告表里医院也复制了这两列,但影像两路先看到会被人口学先验带偏(结构.md 第四部分)。以后 CEUS 定量落地,超声一路再加关键帧和灌注指标。

读资料库要经 cadx.library 的 read_clinical、read_ultrasound、read_mr:读时逐个核对文件的 SHA-256 与清单一致,被改过就报错。

答案库 answers/<构建号>/

manifest.json
snapshot/
  snapshot.json
  build.yaml
  code_maps.yaml      (只在 decode_codes 为 true 时有)
aliases.csv
diagnoses.csv
skipped.csv
review.csv
文件 内容
manifest.json 构建号、跳过清单的说明句(skipped_note)、本目录其余全部文件(含 snapshot/ 下的)的 SHA-256
snapshot/snapshot.json 构建号、完整 git 提交号、工作区是否干净、git diff HEAD 的 SHA-256、Python 版本、uv.lock 的 SHA-256、开始和结束时间、各配置文件的 SHA-256
snapshot/build.yaml 等 这次所用配置的原文副本。配置文件以后改了,这次构建用的是什么仍然查得到
aliases.csv 化名、三位病例号、视频号(U 加病例号)。会诊产物只用化名,评价靠这张表接回真实病例;CEUS 定量的结果按视频号接回
diagnoses.csv 化名、病例号、临床表、超声表、MR 表各自的诊断编码、诊断名(不解码时留空)。三张表各存一份,用哪一份算分由评价决定(进库病例已过 C10,三份相同;分开存是为了保留各表原值,评价口径不必在装配时定)
skipped.csv 跳过清单,见第四节
review.csv 复核清单,见第五节

CSV 用带 BOM 的 UTF-8(文件开头有一个不可见的标记),医生用 Excel 直接打开不会乱码。

.partial 是什么

两个库先写进 <根目录>/<构建号>.partial/,全部写完、清单算完后再改名成 <构建号>/:先改答案库,最后改资料库。所以:

  • 资料库的 <构建号>/ 出现,才算这次构建完成。 会诊只读资料库,读取函数也拒绝名字以 .partial 结尾的目录,没写完的构建不会被用上。
  • 看到 .partial 目录,说明那次构建中途出错。目前已知的一种是某张关键图的格式(位深、颜色类型、透明色)不能逐像素保真重编码:这本是只落在一例上的问题,但现在的代码让整次构建停下,而不是跳过这一例。它不会被自动删除,留着给人查原因;查完由人决定删不删。
  • 只有答案库的 <构建号>/、没有同号的资料库,同样是没完成的构建。
  • 配置、映射表或整张表的检查(T1–T5)不过时,构建在建目录之前就停下,不留任何目录。
  • 目标目录已经存在时报错,不覆盖。构建号带到秒,正常不会重名。

四、跳过清单怎么看

规则

交付资料里的问题分两种(AGENTS.md「错误怎么处理」):

  • 整张表的问题(T1–T5):一例也读不出来,整次构建停下,报错信息以检查编号开头,写明哪个文件哪一项不符。
  • 只落在某一个病例上的问题(C01–C13):跳过这一整例,接着处理下一例,在 skipped.csv 里记一行。这一例不进资料库,也不进答案库的 aliases.csv、diagnoses.csv。

先对三张表的每一行和每个病例目录做 C01(编号认不认得出);认得出的,再逐例按 C02 到 C13 的顺序检查,遇到第一项没过就停下,清单上每例只记这一项(C01 记的是认不出的那一行,不算某一例,见下文)。后面的检查没做过,不代表没问题:医生改好这一项后,重新构建时它可能卡在后面某一项。这句说明写在答案库 manifest.json 的 skipped_note 里,终端也会打印;CSV 文件开头不能加注释行,所以没写在 skipped.csv 里。为什么整例跳过而不是停下整次构建:负责人 2026-10-16「都是遇到整个病例跳过,让医生核对去吧」。评价时要报告跳过了几例、各因为什么。

skipped.csv 的列

列 含义
病例 三位病例号。C01(编号认不出)的行不知道是哪个病人,写成「表的路径 第 N 行」(N 是 Excel 行号,第 1 行是表头);病例目录名不在 1–999 之间时写成「病例目录 目录名」
检查编号 C01–C13
检查说明 这项检查的一句话说明
文件 涉及的文件,相对交付包根目录,多个用「; 」隔开
列 涉及的列,多个用「; 」隔开;不涉及具体列(如目录里的文件不对)时为空
涉及值 JSON 字符串:不合规的原值、对不上的各表的值、命中的词和所在句子等,供医生核对

C01 的行不计入病例数,也不计入终端的「跳过病例」,单独算在「认不出编号的行」里;「按检查编号分的跳过数」里的 C01 就是这些行。同一个病人如果在某张表里编号写坏了,他在 C01 记一条(那一行),又会因为「缺在那张表」在 C03 再记一条,这是预期的。

检查编号速查

规则细节以代码为准:T1–T4 在 src/cadx/delivery/_tables.py,T5 在 src/cadx/build/_config.py,C01–C10 在 src/cadx/delivery/_cases.py,C11–C13 的词表和正则在 src/cadx/delivery/_text.py,表名、工作表名、表头在 src/cadx/delivery/_shape.py。每条规则为什么这样定见任务书 ../任务书/01-装配两个库.md 第二部分。

编号 查什么 不过时
T1 三张汇总表都存在 整次构建停下
T2 每张汇总表恰好有约定的那一个工作表 整次构建停下
T3 表头与约定一字不差(含诊断列名里写的编码含义) 整次构建停下
T4 三个子目录里没有多余的文件:临床、MR 目录只有自己那张汇总表;超声目录只有超声汇总表和三位数字命名的病例目录 整次构建停下
T5 解码时映射表恰好覆盖 17 项,性别、诊断的含义合规(见第一节) 整次构建停下
C01 编号认得出:临床表是 P 加三位数字,两张报告表是补零或不补零的纯数字,病例目录是三位数字;规范化后在 1–999 之间 记一行,不计入病例数
C02 同一张表里这个编号只出现一次 跳过这一例
C03 三张表和病例目录里都有这一例 跳过这一例
C04 病例目录恰好三个文件:U<号>_key.png,加一对配套的视频和单例报告表:U<号>_roi2.mp4 配 U<号>_roi2_report.xlsx,或 U<号>.mp4 配 U<号>_report.xlsx;混搭不过(负责人 2026-10-16 定):单例报告表的名字随视频名配对,不配套时说不清这份报告属于哪个版本的视频,交医生确认 跳过这一例
C05 超声表的「CEUS视频ID」是 U<号>,与病例号一致 跳过这一例
C06 单例报告表的工作表、表头正确,恰好 1 行数据,7 格与超声汇总表对应行逐格相等(类型也要相同) 跳过这一例
C07 临床表取值:性别、年龄、诊断、症状、既往史的取值范围;化验原值和档位要么都空(没测),要么都有值;解码时编码列的值在映射表里 跳过这一例
C08 超声表取值:性别「男」或「女」、年龄形如「数字岁」、诊断 1–4(解码时还要在映射表里)、推注开始时间是非负整数、超声描述非空 跳过这一例
C09 MR 表取值:性别、年龄、诊断同 C08,MR 描述非空 跳过这一例
C10 跨表一致:诊断三张表两两相等;年龄三张表相等(报告表去掉「岁」后和临床表的数字比);性别超声表与 MR 表相等,解码时临床表的性别也要相等 跳过这一例
C11 报告文字里没有结论段标题:段首(全文开头或换行之后)出现「印象」「结论」「提示」等关键词加冒号 跳过这一例
C12 报告文字里没有亚型名(缩写、中文名及其常见变体) 跳过这一例
C13 报告文字里没有人口学信息:没有「数字岁」,没有「男」「女」两个字 跳过这一例

几点说明:

  • 类型按 Excel 读出的原样判断,不做转换:要求整数的格读出 45.0 也不过,字符串不去首尾空格。录入时的这类小毛病可能对应真的录错,不替医生猜。
  • C10 是负责人点名要的:同一个人在不同表里本应一样的值对不上,大概率是录入错误,要拿去找医生改。待定问题/04 记了 40 例里已知的跨表不一致。
  • C11 只认段首的标题。正文中段出现「提示:」这类写法不算不过,这一例照常进库,但它会出现在复核清单里;这种中段的提示该不该给专家,等 ../待定问题/06-超声描述中段的造影提示.md 定。在那之前,不能用构建做正式会诊。
  • C12 不查单独出现的「浆液性」「黏液性」:它们在描述囊液性状时是正常用语。
  • C13 禁单字「男」「女」:描述病灶用不到这两个字,出现了多半是写了患者性别,影像两路不该看到。

五、review.csv 是什么

复核清单:进库病例的超声描述、MR 描述里,含「考虑」「可能」「倾向」「提示」任一词的句子,每句一行(化名、路、命中的词、所在句子)。句子按「。」和「;」切(先做 NFKC,全角分号变成半角)。只扫进库的病例,跳过的病例不在这里。

这些词不带冒号时不算检查不过,因为它们也常用来描述影像所见;但它们可能引出医生的诊断倾向,所以列出来由负责人过目,决定要不要改规则或问医生。这里用化名而不是病例号,过目时如需对回病例,查同目录的 aliases.csv。

六、交给医生之前

  • answers/ 里有真实病例号和报告原句(skipped.csv 的涉及值、review.csv 的句子、aliases.csv、diagnoses.csv),只在本机,不进 git(.gitignore 和 pre-commit 两道拦截)。不能贴进文档、提交说明、Linear 评论或发给外部模型服务(AGENTS.md 硬性约束 3)。交给医生核对要走负责人认可的渠道,由负责人转交。
  • 交给医生的通常只有 skipped.csv,说明里带上 skipped_note 那句话:每例只记第一项没过的检查。
  • 不要把 diagnoses.csv 和资料库放在同一个地方给任何会诊代码或模型读(AGENTS.md 硬性约束 1)。
  • 汇报、终端输出、日志里只写计数和检查编号,不写病例号和资料内容。
  • 医生改好交付资料后,重新构建一次,得到新的构建号;旧的构建目录不删、不改(AGENTS.md「两档标准」:运行产物只追加)。