外观
从 PDF 到可研究数据:一次目录抽取的四道校验
任务:政策文件下发的是 PDF,你要的是能跑分组、能做比对的结构化数据。中间这一段怎么走,以及怎么知道自己走对了。 这个案例的价值不在最终那份表,在中途踩的两个坑——它们都不报错。
关于本书出现的「3.0 预研结果」
国家按病种付费 3.0 版分组方案目前处于征求意见稿阶段。国家医保局公开口径为:3.0 版分组方案拟于 2026 年 7 月发布、2027 年 1 月正式执行(《医保支付方式改革有关情况介绍(第 2 期)》,2026-06-03)。
因此:
- 本书中标为「3.0 预研」的分组结果,由 MedGroup 依据 DRG 3.0 版分组方案征求意见稿与 DIP 3.0 版分组征求意见函构建,属实测数据(系统结果),不是官方文件级的政策事实。
- 这些结果不得用于测算、申诉、协商或结算沟通,也不能作为编码调整依据。
- 征求意见稿与最终发布版本可能不同。3.0 正式发布后,本书涉及 3.0 的全部表述与截图将统一更新,更新记录见更新日志。
- 与之对照的「当前全国版」是 MedGroup 页面当前使用的规则口径(产品内部映射为 CHS2.0),对应国家 2.0 版分组方案。
来源限制
源文件为随通知下发的目录附件与政策 PDF,未取得对应发文文号。本案例属实测数据——抽取与校验过程的记录,不是官方发布的目录数据。
具体条目与分值以当地正式文件为准。这里演示的是方法与校验,不是数据本身。
1. 起点与终点
起点:一份 7 页的政策 PDF,加若干张附表。
终点:8 张结构化表(目录、CCI、严重程度、肿瘤、年龄、机构系数、药品、政策正文),能直接喂给分组器。
中间:四个步骤,其中两个是修复步骤——它们是被发现的问题倒逼出来的,不是一开始就设计好的。
2. 四步链条
| 步骤 | 做什么 | 为什么需要它 |
|---|---|---|
| 1 抽取 | PDF → 8 张 CSV/表 | — |
| 2 编码修复 | 把 <e5><b8><b8> 还原成汉字 | 抽取产物里的中文成了十六进制转义 |
| 3 导出修复 | 修 R 侧导出的 xlsx/json 里同类问题 | 换个工具导出,同样的字符问题又出现一次 |
| 4 校验 | 36 项检查逐项出数 | 前三步是否真的做对了,不能靠看 |
步骤 2 和 3 是这个案例的重点。 它们的存在本身就是结论:抽取一次跑通了,不代表抽对了。
3. 第一个坑:中文变成十六进制
PDF 抽出来的表格,行数对、列数对、结构完整,但中文长这样:
text
<e5><b8><b8><e5><be><b7><e5><b8><82>这是 PDF 里嵌入子集字体时的典型现象——文字的字形在,字符编码丢了。
危险在于:
- 不报错。程序照常跑完
- 行数正确。任何基于行数的检查都会通过
- 只有中文受影响。编码、分值这些数字列看着完全正常
所以如果你只核对"目录有多少条、分值对不对",这个问题能一路带到下游,直到某天有人问"为什么病种名称是乱码"。
修法是确定的**:把连续的 <xx> 序列按十六进制解析回字节,再按 UTF-8 解码。
python
def decode_angle_hex(text):
# 把 <e5><b8><b8> 这样的连续块还原成汉字
...注意它出现了两次:第一次在 Python 抽取产物里,第二次在 R 侧导出的 xlsx 与 json 里。换个工具,同样的坑再踩一次——所以修复要针对"产物"做,不是针对"某一个工具"做。
4. 第二个坑:79 行代表 6742 条
原始目录 8718 行。但其中 79 行是区间写法——一行代表一个编码范围内的多个病种。
展开之后是 15381 行。
| 阶段 | 行数 |
|---|---|
| 原始抽取 | 8718 |
| 其中区间行 | 79 |
| 区间展开生成 | 6742 |
| 展开时跳过(已存在) | 1422 |
| 最终目录 | 15381 |
对账关系:8718 − 79 + 6742 + ... 各项能对上,所以展开逻辑本身是可验的。
不展开会怎样: 上一年目录是 7085 条。拿 8718 去比,你会得到"新增一千多、删除若干"的差异清单——而正确答案是拿 15381 去比。
差异清单会整体错误,并且:
- 不报错
- 内部自洽
- 每一条单独看都"讲得通"
这是本案例最值得记住的一点。
5. 36 项校验都查了什么
抽取和修复做完,跑一遍校验。分四组:
组一:原始抽取完整性(5 项)
目录、CCI、严重程度、肿瘤、年龄五张表,逐张核实际行数 == 期望行数。期望值来自文件本身写明的条目数,不是"上次跑出来是多少"。
| 表 | 行数 |
|---|---|
| 目录 | 8718 |
| CCI | 105 |
| 严重程度 | 97 |
| 肿瘤 | 12 |
| 年龄 | 31 |
| 机构系数 | 297 |
| 药品 | 89 |
同时核序号连续性:最小值 1、最大值 == 行数、去重个数 == 行数——三者同时成立才说明无跳号无重号。
组二:目录结构(6 项)
- 展开后行数 == 15381
- DIP 编码唯一(去重计数 == 总行数)
- 基准分值无空
- 诊断编码无空
- 操作编码无空
- 区间行已从目录中移除(避免既有区间行又有展开行)
组三:分类计数(7 项)
按病种类型与操作范围逐项核:
| 类型 | 条数 |
|---|---|
| 综合病种 | 8166 |
| 核心病种 | 6269 |
| 基层病种 | 946 |
综合病种再按操作范围拆:BSZL 2066、D 2066、T 2066、S 1968。
这一组的价值在于交叉验证:类型计数之和必须等于目录总数,操作范围计数之和必须等于综合病种数。任何一处抽取错位,这些和就对不上。
组四:规则表与目录的一致性(18 项)
- 规则表里的 DIP 编码是目录编码的子集(不能有目录里没有的组)
- 规则表主诊断、操作编码无空
- 特殊操作标识都在允许集合内(
BSZL、SS-三、ZDXCZ、ZLXCZ-三) - 规则表的操作都被操作分组表覆盖
- 打包出的 JSON 长度与源表一致
- 预览文件的城市、年度、来源范围、各项计数与实际一致
结果:36 项全过,0 项失败。
6. 这份验收记录长什么样
三份机器可读的 JSON,各管一段:
| 文件 | 管什么 | 关键内容 |
|---|---|---|
*_extraction_validation.json | 抽取是否完整 | 逐表 rows / expected_rows / row_match,序号连续性 |
*_rule_validation.json | 规则包是否自洽 | 36 项 {name, passed, detail},加汇总计数 |
*_compare_validation.json | 跨年比对是否成立 | 两版计数、added_removed_kept_match 恒等式 |
机器可读是关键:下次换版,把新文件名代进去重跑,几秒钟就知道通不通过。目视检查做不到这一点。
7. 这个案例可以怎么复用
对着自己手上的目录抽取产物,按顺序问:
- 中文是不是字? 搜一下
<和?,命中数应为 0 - 行数对不对? 与文件写明的条目数比,不接受"差不多"
- 序号连不连续?
max == 去重个数,且最小值为 1 - 有没有区间行? 有就必须展开,展开前后要能对账
- 主键唯不唯一? 去重计数 == 总行数
- 分类计数加不加得起来? 各类之和 == 总数
- 规则表是不是目录的子集? 出现目录里没有的编码就是错
七个问题,每个都输出一个数,写进一个文件。 这就是一份最小可用的验收记录。
8. 这个案例不能推出什么
- 不能推出所有 PDF 抽取都会遇到这两个问题。 取决于 PDF 怎么生成的、用什么工具抽。但两个都不报错这一点是普遍的。
- 不能推出 36 项就够了。 检查项要按目录的实际结构定,这里的 36 项对应这份目录的 8 张表。
- 不能推出这份目录数据可以直接使用。 见开头的来源限制。
- 不能推出校验全过就一定对。 校验只能证明"没有被检出的错误"。真正的兜底是形态二的全量回归——见 别人给你一份规则包,怎么验它对不对。
9. 相关篇目
- 验收的完整框架:别人给你一份规则包,怎么验它对不对
- 归一化没做会怎样:征求意见稿和正式版,到底差了多少
- 怎么判断源文件现行有效:怎么读一份地方文件