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

213 lines
21 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 装配两个库
读者:要跑一次「装配两个库」、或者要把跳过清单交给医生的人。这一步读入医院的交付包,逐例检查,把检查全部通过的病例写成两个库:给 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` 尚未建)。拿到后按下面的格式写:每个编码列一项,列名映射到「数字 → 含义」。
```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(报告表的诊断)跳过。
## 二、怎么跑
```bash
uv run python scripts/build.py --config config/build.yaml
```
脚本只接收 `--config` 一个参数,不能在命令行单独改某个参数,这次用了什么参数只看配置文件就知道(`结构.md` 第五部分「配置」)。
终端只打印计数:构建号、两个库的路径、病例总数、进库病例数、跳过病例数、认不出编号的行数、按检查编号分的跳过数、复核清单条数、跳过清单的说明句。不打印病例号和任何资料内容,终端输出可以贴进汇报。
跑完建议再做一次泄露扫描:
```bash
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`「两档标准」:运行产物只追加)。