尧图网络 高端网站定制 · 原创设计
免费咨询热线
400-888-6620
免费获取方案
CHS-DRG分组器开发实战:Python病案首页数据提取与ICD线性化处理
简介这份资源是一套基于Python实现的CHS-DRG分组器面向医疗机构信息科人员、医保数据分析师及医疗信息化方向的学习者用于解决主要诊断类别MDC对应关系、基础诊断相关组ADRG判定规则以及严重并发症与合并症MCC/CC排除条件的结构化提取与标准化输出问题。压缩包共约2000个文件、7.32MB其中886个py为核心程序模块624个pyc为编译字节码343个zbak为备份文件另有125个txt文档、18个dat数据集及1个db数据库并附带版本管理配置与授权协议目录结构完整便于按模块阅读与二次开发。目前已有121人学习下载。读者可借此理解DRG分组运算的工程实现思路掌握诊断与手术信息的映射处理、MCC/CC排除逻辑的代码组织方式并获得可直接运行调试的脚本与数据样例适合作为医疗质量分析与分组算法研究的实践参考。1. 从一份病案首页到 DRG 分组为什么 Python 是绕不开的那把刀病案首页填完、编码员点下保存的那一刻数据其实还是生的——诊断和手术是一串带扩展位的 ICD 码年龄是出生日期费用是一堆明细。真正决定这家医院能不能拿到合理医保支付的是后面那一步把首页数据喂进 DRG 分组器吐出一个四位数的 DRG 组别和对应的权重。我见过太多团队卡在这一步不是分组逻辑不会而是数据提取和线性化处理没做干净导致分组器要么报错要么分出来的组看着对、细看全是坑。CHS-DRG 的分组不是一次查表而是先分 MDC、再分 ADRG、最后进 DRG的三级漏斗每一级都依赖前一级清洗后的结构化输入。Python 在这里的价值不是能写脚本而是能把 ICD 码的线性化、主诊断/主手术的判定、CC/MCC 排除表匹配这几件脏活串成一条可复现的流水线。这篇写给正在做医保结算清单或 DRG 分组工具的开发者从数据提取讲到线性化处理再到分组器骨架每一步都给能跑的代码和参数说明。2. CHS-DRG 分组器的数据底座从病案首页提取什么、怎么提2.1 先搞清楚分组器到底吃哪几列数据很多人一上来就写分组逻辑结果发现字段对不上。CHS-DRG 分组器的输入可以粗暴归成四类患者基本信息性别、年龄、新生儿体重、离院方式、诊断信息主诊断 其他诊断带 ICD-10 码、手术操作信息主手术 其他手术带 ICD-9-CM-3 码、费用与资源信息总费用、住院天数。其中年龄不是直接给的得从出生日期和入院日期算新生儿体重只在特定 MDC 里参与判断缺了会直接掉进错误组。我一般会先定义一个提取配置把哪些列映射到分组器的哪个字段显式写出来而不是散落在代码各处。这样换一家医院的首页模板只改配置不改逻辑。# config.py # 病案首页列名 - 分组器标准字段的映射配置 # 不同医院的首页导出列名差异极大用配置隔离 FIELD_MAPPING { patient_id: 病案号, gender: 性别, # 1男 2女 birth_date: 出生日期, admit_date: 入院日期, discharge_date: 出院日期, age: 年龄, # 若首页直接给年龄则用否则由日期算 neonate_weight: 新生儿出生体重, # 单位克 discharge_mode: 离院方式, # 1医嘱离院 2医嘱转院 ... total_cost: 总费用, main_diag: 主要诊断编码, other_diags: 其他诊断编码, # 多值分隔符需统一 main_proc: 主要手术操作编码, other_procs: 其他手术操作编码, } # 多值字段的分隔符各院导出可能是逗号、分号或竖线 MULTI_VALUE_SEP ,这段配置的核心作用是解耦。参数上要注意两点一是gender的编码各院可能是 1/2 也可能是 M/F映射后要统一二是多值字段的分隔符必须确认我踩过用逗号切结果 ICD 码里本身带逗号的坑所以后面还要做一次格式校验。2.2 用 pandas 做提取与字段标准化提取阶段最容易被忽略的是空值语义。首页里一个空的其他诊断可能是确实没有也可能是没填这两者对分组的影响完全不同——前者不影响后者可能导致 CC/MCC 漏判。我的做法是提取时保留原始空值标准化阶段再统一成空列表并在日志里记录每份病案的缺失字段数。import pandas as pd from config import FIELD_MAPPING, MULTI_VALUE_SEP def extract_homepage(raw_df: pd.DataFrame) - pd.DataFrame: 从原始首页 DataFrame 提取分组器所需字段并标准化 df pd.DataFrame() for std_field, src_col in FIELD_MAPPING.items(): if src_col not in raw_df.columns: # 缺列不静默跳过记录后补空避免后续 KeyError print(f[WARN] 缺少列: {src_col} - {std_field}) df[std_field] None else: df[std_field] raw_df[src_col] # 多值字段统一切成列表空值 - 空列表 for col in [other_diags, other_procs]: df[col] df[col].apply( lambda x: [] if pd.isna(x) or str(x).strip() else [c.strip() for c in str(x).split(MULTI_VALUE_SEP) if c.strip()] ) # 数值字段强制转型非法值置 NaN 并计数 for col in [total_cost, neonate_weight]: df[col] pd.to_numeric(df[col], errorscoerce) return df逻辑说明先按配置逐列搬运缺列时打印警告而不是抛异常保证批量处理不中断多值字段统一成 Python 列表后续线性化处理直接迭代数值字段用errorscoerce把脏值转成 NaN方便统计。参数上MULTI_VALUE_SEP如果某院用的是分号改配置即可不用动函数。2.3 年龄与住院天数的派生计算年龄在 DRG 分组里是硬门槛比如某些 MDC 对年龄有明确切分。首页给的年龄有时是岁有时是月还有的是天直接拿来用会翻车。稳妥做法是用出生日期和入院日期算算不出来再退回首页年龄字段。import pandas as pd def derive_age_and_los(df: pd.DataFrame) - pd.DataFrame: 派生年龄岁和住院天数 admit pd.to_datetime(df[admit_date], errorscoerce) birth pd.to_datetime(df[birth_date], errorscoerce) discharge pd.to_datetime(df[discharge_date], errorscoerce) # 年龄按周岁算不足一岁记 0分组器对 1 岁另有规则 age_days (admit - birth).dt.days df[age_years] (age_days // 365).where(age_days 0, otherpd.NA) # 住院天数出院-入院至少 1 天 los (discharge - admit).dt.days df[los] los.where(los 1, other1) return df这里的关键参数是年龄的取整方式。用// 365是向下取整和多数分组器周岁口径一致如果某地要求按虚岁或按月龄改这一行即可。住院天数强制最小为 1是因为 0 天在部分权重计算里会触发除零或异常分组。3. ICD 码线性化处理把带扩展位的编码压成分组器认的形态3.1 线性化到底在做什么为什么不能直接查表CHS-DRG 分组器匹配的是线性化后的 ICD 码不是首页上那串带扩展位的原始码。所谓线性化核心是三件事去掉不影响分组的扩展位、把编码统一成标准格式、把类目码和亚目码的层级关系理清。举个常见情况首页可能填I50.900分组器要的是I50.9或I50具体压到哪一级取决于分组规则表里用的是哪一级。我见过最典型的翻车是编码员填了带星剑号的码或者填了未特指的.9线性化时没处理结果 CC/MCC 判定全错。所以线性化不是简单的字符串截断得有一张规则表驱动。3.2 用规则表驱动线性化而不是硬编码截断硬编码code[:5]这种写法迟早出事因为不同章节的编码长度规则不一样。正确做法是维护一张原始码模式 - 线性化码的映射表Python 只负责查表和兜底。import re # 线性化规则按正则匹配命中则替换 # 顺序敏感先匹配具体规则再走通用兜底 LINEARIZE_RULES [ (re.compile(r^([A-Z]\d{2})\.(\d)00$), r\1.\2), # I50.900 - I50.9 (re.compile(r^([A-Z]\d{2})\.(\d{2})0$), r\1.\2), # I50.910 - I50.91 (re.compile(r^([A-Z]\d{2})\.(\d)$), r\1.\2), # 已是标准形态 (re.compile(r^([A-Z]\d{2})(\d)$), r\1.\2), # I509 - I50.9 ] def linearize_code(code: str) - str: 将单个 ICD 码线性化为分组器标准形态 if not code or not isinstance(code, str): return code code.strip().upper().replace( , ) for pattern, repl in LINEARIZE_RULES: if pattern.match(code): return pattern.sub(repl, code) # 兜底无法识别的码原样返回并记录人工复核 print(f[REVIEW] 未匹配线性化规则: {code}) return code逻辑说明规则按从具体到通用排列re.sub用捕获组保留章节和亚目部分。参数上LINEARIZE_RULES的顺序不能乱否则I50.900可能被通用规则先截成I50.9之外的东西。兜底返回原码并打[REVIEW]标记是为了让编码员能捞出来人工核对而不是悄悄放过。3.3 批量线性化与主诊断/主手术的判定单码线性化只是第一步真正影响分组的是哪个是主诊断、哪个是主手术。首页通常已经标了主诊断但主手术有时需要按规则判定——比如多个手术时取与主诊断最相关、或创伤最大的那个。我一般先信任首页标注再用规则做一致性校验。def linearize_record(record: dict) - dict: 对一份病案的所有诊断和手术做线性化 record[main_diag_lin] linearize_code(record.get(main_diag, )) record[other_diags_lin] [linearize_code(c) for c in record.get(other_diags, [])] record[main_proc_lin] linearize_code(record.get(main_proc, )) record[other_procs_lin] [linearize_code(c) for c in record.get(other_procs, [])] # 一致性校验主诊断不应出现在其他诊断里 if record[main_diag_lin] in record[other_diags_lin]: print(f[WARN] 病案 {record.get(patient_id)} 主诊断重复出现在其他诊断) return record参数说明main_diag_lin和other_diags_lin分开存是为了后续 CC/MCC 匹配时能明确排除主诊断本身。一致性校验那几行看着简单但能捞出一批首页填写错误属于低成本高回报的检查。4. 分组器骨架MDC、ADRG、DRG 三级漏斗的 Python 实现4.1 三级漏斗的分组顺序不能颠倒CHS-DRG 的分组是严格顺序的先根据主诊断定 MDC主要诊断大类再在 MDC 内根据主诊断和主手术定 ADRG核心疾病诊断相关组最后在 ADRG 内根据 CC/MCC 和年龄等条件定 DRG。顺序颠倒会导致 ADRG 匹配不到因为 ADRG 表是按 MDC 分区的。我用一个分组器类把三级逻辑串起来每级失败都返回明确的错误码而不是抛异常中断整批处理。class DRGGrouper: def __init__(self, mdc_table, adrg_table, drg_table, cc_exclude_table): self.mdc_table mdc_table # 主诊断 - MDC self.adrg_table adrg_table # (MDC, 主诊断, 主手术) - ADRG self.drg_table drg_table # (ADRG, CC/MCC标志) - DRG self.cc_exclude_table cc_exclude_table # 主诊断 - 需排除的 CC 码 def group(self, record: dict) - dict: mdc self._match_mdc(record) if not mdc: return {drg: None, error: MDC_NOT_FOUND} adrg self._match_adrg(record, mdc) if not adrg: return {drg: None, error: ADRG_NOT_FOUND} drg self._match_drg(record, adrg) return {drg: drg, mdc: mdc, adrg: adrg, error: None} def _match_mdc(self, record): return self.mdc_table.get(record[main_diag_lin]) def _match_adrg(self, record, mdc): key (mdc, record[main_diag_lin], record[main_proc_lin]) return self.adrg_table.get(key)逻辑说明group方法按 MDC - ADRG - DRG 顺序调用每级返回 None 就带错误码退出。参数上四张表建议用 dict 或 DataFrame 索引匹配复杂度 O(1)。错误码MDC_NOT_FOUND和ADRG_NOT_FOUND要分开统计前者多半是主诊断码有问题后者多半是主手术缺失或码不对。4.2 CC/MCC 判定排除表是分组的胜负手CC并发症合并症和 MCC严重并发症合并症的判定直接决定 DRG 落在哪个权重档。判定逻辑是遍历其他诊断看是否在 CC/MCC 表里同时要排除主诊断本身对应的 CC 码——这就是cc_exclude_table的作用。def judge_cc_mcc(record: dict, cc_table: dict, exclude_table: dict) - str: 返回 MCC / CC / NONE main_diag record[main_diag_lin] excluded set(exclude_table.get(main_diag, [])) has_mcc, has_cc False, False for code in record[other_diags_lin]: if code in excluded: continue level cc_table.get(code) if level MCC: has_mcc True elif level CC: has_cc True if has_mcc: return MCC if has_cc: return CC return NONE参数说明cc_table是码到等级的映射exclude_table是主诊断到应排除码列表的映射。注意excluded用 set 是为了 O(1) 查找其他诊断多的时候性能差别明显。返回NONE表示无 CC/MCC分组时走基础档。4.3 把三级结果拼成最终 DRG 并输出最后一步是把 ADRG 和 CC/MCC 标志拼成 DRG 码通常 DRG 码是 ADRG 加一位数字后缀。这一步查drg_table即可但要处理该 ADRG 不接受 CC/MCC的情况。def _match_drg(self, record, adrg): cc_level judge_cc_mcc(record, self.cc_table, self.cc_exclude_table) # 部分 ADRG 只有基础档CC/MCC 不改变分组 key (adrg, cc_level) drg self.drg_table.get(key) if drg is None: drg self.drg_table.get((adrg, NONE)) return drg逻辑说明先按(adrg, cc_level)查查不到再退回(adrg, NONE)兼容那些不区分 CC/MCC 的 ADRG。参数上drg_table的键设计成元组避免字符串拼接带来的歧义。5. 避坑与排查线性化和分组里最容易翻车的五件事5.1 现象分组结果大批落在错误组错误码全是 ADRG_NOT_FOUND原因主手术码线性化后和 ADRG 表里的码对不上最常见的是手术码少了小数点或多了扩展位。解决把[REVIEW]标记的码全部导出和 ADRG 表里的手术码做一次集合差集差异码集中处理。我一般会写个一次性脚本对比而不是逐个猜。5.2 现象CC/MCC 判定结果比预期多权重虚高原因cc_exclude_table没配全主诊断对应的排除码漏了导致本应排除的其他诊断被算成 CC。解决核对排除表是否覆盖了所有主诊断章节尤其是那些主诊断本身即 CC的情况。这个坑很隐蔽因为分组不报错只是钱算多了。5.3 现象年龄字段大量为空分组掉进新生儿或老年组原因出生日期或入院日期格式不统一pd.to_datetime解析失败变成 NaT年龄算出来是 NA。解决提取阶段先统计日期字段的格式分布对多种格式分别指定format参数解析解析失败的单独输出人工处理。5.4 现象多值诊断切分后码被截断原因分隔符配置错误或者 ICD 码本身含分隔符。解决先抽样看原始字段确认分隔符切分后校验每个码是否符合 ICD 码正则不符合的整条记录打标复核。5.5 现象批量处理跑到一半内存爆掉原因一次性把全量首页读进内存且线性化时对每行都做正则匹配。解决用pd.read_csv(chunksize...)分块读线性化结果做缓存同一个码不重复匹配我实测缓存能把大样本的处理时间压掉一大半。6. 让分组器可验证用构造样本做回归测试的一个习惯分组器写完最怕的是改了一处规则别处悄悄坏了。我的习惯是维护一组构造样本——不是真实病案而是手工编的、覆盖各 MDC 和 CC/MCC 分支的最小用例每次改规则先跑这组样本。下面是一个最小测试骨架。import pytest from grouper import DRGGrouper # 构造样本覆盖基础档、CC 档、MCC 档各一例 SAMPLES [ { patient_id: T001, main_diag_lin: I50.9, other_diags_lin: [], main_proc_lin: , expect_adrg: FR1, expect_cc: NONE, }, { patient_id: T002, main_diag_lin: I50.9, other_diags_lin: [N18.5], # 假设为 CC main_proc_lin: , expect_adrg: FR1, expect_cc: CC, }, ] def test_group_consistency(grouper: DRGGrouper): for s in SAMPLES: result grouper.group(s) assert result[adrg] s[expect_adrg], f{s[patient_id]} ADRG 不符 cc judge_cc_mcc(s, grouper.cc_table, grouper.cc_exclude_table) assert cc s[expect_cc], f{s[patient_id]} CC 判定不符这段测试的价值不在覆盖率而在锁行为。参数上expect_adrg和expect_cc是人工确认过的期望值一旦规则改动导致不符测试立刻红。我一般还会加一个全量样本分组结果快照的对比把上次分组结果存成文件这次跑完 diff差异超过阈值就人工看。一个具体技巧线性化函数的缓存用functools.lru_cache包一层因为同一批病案里重复码极多缓存命中率通常很高。from functools import lru_cache lru_cache(maxsize10000) def linearize_code_cached(code: str) - str: return linearize_code(code)maxsize设 10000 是因为 ICD 码总量有限基本能全缓存。注意lru_cache要求参数可哈希字符串没问题但如果以后要传列表就得改设计。最后说个我自己的教训早期我图快把线性化规则直接写死在分组逻辑里结果一次编码版本更新规则要改我花了整整两天在几千行代码里找散落的截断逻辑。从那以后凡是码到码的转换一律走独立规则表加缓存规则表和分组逻辑物理隔离。这个习惯让我后面每次版本切换都能在一小时内完成回归。希望帮到你。本文还有配套的精品资源点击获取
RELATED

相关推荐

开放智能体安全平台:从提示词约束到策略引擎的智能体行为管控

开放智能体安全平台:从提示词约束到策略引擎的智能体行为管控

1. 从“上锁”说起:这个平台到底在解决什么问题第一次看到“开放智能体安全平台”这个说法,我脑子里蹦出来的画面是给一台高速运转的工业机器人加装围栏和急停按钮。智能体这东西,本质上就是让大模型从“只会聊天”进化到“能动手做事”——它…

📅 2026/10/20 8:24:49
MySQL全量备份深度解析:mysqldump参数、恢复演练与自动化脚本

MySQL全量备份深度解析:mysqldump参数、恢复演练与自动化脚本

备份这件事,我在生产环境里见过太多"翻车"现场了。有一回半夜接到电话,说某个核心库的表被人误删了,需要立刻恢复。等我连上去一看,备份脚本倒是每天都在跑,文件也躺在磁盘里,结果一执行恢复&…

📅 2026/10/20 8:24:49
自由能原理如何驱动神经元集群:从预测编码到最小自由能机器

自由能原理如何驱动神经元集群:从预测编码到最小自由能机器

这篇内容想讲清楚一件我一直觉得很有意思的事:为什么一个由上千亿神经元组成的集群,不会陷入混乱,反而总能在不断变化的环境中保持稳定、做出反应、形成记忆。这个问题我研究了好几年,越深入越发现,答案很可能落在一个…

📅 2026/10/20 8:24:49
MORE NEWS

更多资讯

📰

从OpenClaw到Moltbook:让纸质书变身AI可调用的个人知识库

不知道大家有没有刷到过一条帖子,标题写得相当上头:"彻底疯狂了,OpenClaw 社区:Moltbook 硅基觉醒中..."。我习惯性地划过去,又忍不住划回来,仔细把标题拆了一遍。说实话,这种表达方式…

📰

Spring单例注入多例Bean失效?四种方案彻底搞懂Bean作用域

作为常年跟Spring打交道的人,一定遇到过这种诡异情况:明明把某个Bean定义成了Scope("prototype"),信心满满地注入到另一个单例Bean里,结果每次调用拿到的却是同一个对象,内部状态乱得让人怀疑人生。我最早掉…

📰

C++内存管理实战:从RAII到内存池,根治泄漏与性能问题

内存管理是C开发者绕不开的一座山。不管是刚写完第一行new的新手,还是在线上环境被Segment Fault折磨过几轮的老人,最终都会发现:真正决定C代码质量的,往往不是算法有多巧妙,而是对内存的控制有多精细。这篇东西我不打…

📰

Python+HTML四足机器人DIY:舵机抖动与步态调参实战

简介:这份资源是面向零基础爱好者的四足机器人DIY完整指南,以Python编程与HTML界面开发为核心,帮助新手从理论到实践搭建可操控的四足机器人。包内共38个文件,约66.39MB,包含13个Python脚本负责运动控制与算法实现&…

📰

开源AI助理OpenClaw接入飞书:从零搭建省下年费几百元

前阵子我把开源助理项目OpenClaw接到了飞书上,团队群里原来按月付费的那个外挂机器人当天就退役了。算下来一年省了六百多,而且接的是自己的模型,数据也不会经过第三方平台,体验反而比之前更顺手。这篇文章就是把整个安装和对接过…

📰

AI写作工具怎么选?从产品思路到真实体验的千笔AI评测

今年年初那阵子,我把市面上能叫得上名字的AI写作工具挨个试了一遍,最后长期留在工作流里的,是千笔AI。不是因为闲,而是当时我的状态真的很狼狈——公众号一周要出四篇原创,短视频脚本还得另算,每次坐到电脑…

TODAY

今日更新

THIS WEEK

本周精选

THIS MONTH

本月热门

读完文章,想聊聊您的网站?

告诉我们您的行业与需求,资深顾问一对一梳理方案与报价,全程免费。

📞 💬