assets/2026-07-01-医数智联系列开篇-医院数字化工作者的工具箱_2026-07-01_18-28-11.jpg

医数智联·第 17 期:Python + 首页数据自动校验 —— 300 行代码,救回 N 千万医保损失

[!ABSTRACT] 核心摘要

  • 本期主题:Python 自动校验首页数据——从「人工抽查 5%」到「机器全检 100%」
  • 核心结论:首页校验的本质 = 「规则 + 自动化」,把 50+ 条校验规则写成 Python 脚本,5 分钟跑完 1 万份病案
  • 5 大校验类型:必填校验、格式校验、范围校验、关联一致性、ICD 编码映射
  • 实战效果:从「每月抽查 100 份」变成「每月全检 1 万份」,问题发现率提升 100 倍
  • 阅读时长:约 10 分钟

一、场景引入:为什么医院需要「首页自动校验」?

[!example] 一个真实的月底
月底,医保局要结算上月的病案。

信息科从 HIS 导出 1 万份病案首页,质管办抽查 100 份,发现 12 份有问题

按这个抽样率推算,全院可能有 1200 份有问题——这些将被医保局拒付

1 万份病案,平均每份 5000 元,1200 份 × 5000 = 600 万

院长:「为什么不能全检?

信息科:「1 万份人工全检要 50 人天。」

院长:「……那先这样吧。」

这是医院首页校验的典型困境——抽样检不出问题,全检成本太高

现状 痛点
人工抽查 5% 95% 的问题漏检
人工全检 成本太高,不可持续
抽查 → 整改 总是「事后补救」
医保拒付 一年损失几百万

[!quote] 狼叔的判断
“首页校验不是’要不要做’,是’用机器做还是人工做’。” 这一期,带你用 Python 写一个完整的首页校验脚本,300 行代码,5 分钟全检 1 万份


二、工具原理:首页校验的 5 大类型

2.1 校验类型 ①:必填校验

[!info] 必填字段不能为空
例:病案号、出院日期、主诊 ICD、责任医生,这 4 个必填,缺一不可

2.2 校验类型 ②:格式校验

[!info] 字段格式必须正确

  • 病案号格式:BA + 8 位数字(如 BA20260715001)
  • 出院日期:YYYY-MM-DD
  • 主诊 ICD:字母 + 数字 + .数字(如 I10.x05)

2.3 校验类型 ③:范围校验

[!info] 数值在合理范围

  • 年龄:0-150 岁
  • 住院天数:0-365 天
  • 费用:> 0

2.4 校验类型 ④:关联一致性

[!info] 字段之间逻辑一致

  • 出院日期 > 入院日期
  • 主诊 ICD 与性别一致(如「前列腺疾病」不应出现在女性病案)
  • 主诊与重要操作匹配(如「肺部感染」主诊不应有「心脏手术」)

2.5 校验类型 ⑤:ICD 编码映射

[!info] ICD 编码是否合法

  • 是否在 ICD-10 字典里
  • 是否与诊断名称匹配
  • 是否为有效码(非 xxx.x00 占位码)

三、医院实战:完整的首页校验脚本

3.1 准备数据

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import pandas as pd
import re

# 从 HIS 导出本月首页
df = pd.read_excel('本月首页.xlsx')

# ICD-10 字典(从国家卫健委下载)
icd_dict = pd.read_excel('ICD-10字典.xlsx') # 列:ICD编码, 诊断名称, 是否有效
valid_icd = set(icd_dict['ICD编码'])

# 性别限制字典(某些疾病只发生在特定性别)
gender_restricted = {
'前列腺': '男', '子宫': '女', '卵巢': '女', '睾丸': '男',
'妊娠': '女', '前列腺增生': '男', '卵巢囊肿': '女'
}

# 主诊与操作匹配字典(内科主诊不应有外科大手术)
外科主诊关键词 = ['骨折', '损伤', '创伤', '肿瘤', '结石']
内科操作关键词 = ['CT', 'MRI', '超声', '化验', '活检']

3.2 校验函数:必填校验

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
def check_required(df):
"""必填字段校验"""
errors = []
必填字段 = ['病案号', '出院日期', '主诊ICD', '责任医生工号']

for idx, row in df.iterrows():
for field in 必填字段:
if pd.isna(row[field]) or str(row[field]).strip() == '':
errors.append({
'病案号': row.get('病案号', f'第{idx}行'),
'错误类型': '必填校验',
'错误描述': f'字段 [{field}] 不能为空',
'错误等级': '严重'
})
return pd.DataFrame(errors)

3.3 校验函数:格式校验

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
def check_format(df):
"""字段格式校验"""
errors = []

# 病案号格式:BA + 8-12 位数字
病案号_pattern = re.compile(r'^BA\d{8,12}$')

# ICD 格式:字母 + 数字 + 可选 .数字
icd_pattern = re.compile(r'^[A-Z]\d{2}(\.\d{1,3})?$')

for idx, row in df.iterrows():
# 病案号格式
if pd.notna(row['病案号']) and not 病案号_pattern.match(str(row['病案号'])):
errors.append({
'病案号': row['病案号'],
'错误类型': '格式校验',
'错误描述': f"病案号格式错误(应为 BA+8-12 位数字):{row['病案号']}",
'错误等级': '严重'
})

# ICD 格式
if pd.notna(row['主诊ICD']) and not icd_pattern.match(str(row['主诊ICD']).strip()):
errors.append({
'病案号': row['病案号'],
'错误类型': '格式校验',
'错误描述': f"主诊 ICD 格式错误:{row['主诊ICD']}",
'错误等级': '严重'
})

return pd.DataFrame(errors)

3.4 校验函数:范围校验

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
def check_range(df):
"""数值范围校验"""
errors = []

for idx, row in df.iterrows():
# 年龄范围
if pd.notna(row.get('年龄')):
年龄 = float(row['年龄'])
if not (0 <= 年龄 <= 150):
errors.append({
'病案号': row['病案号'],
'错误类型': '范围校验',
'错误描述': f"年龄超范围(0-150):{年龄}",
'错误等级': '一般'
})

# 住院天数范围
if pd.notna(row.get('住院天数')):
天数 = float(row['住院天数'])
if not (0 <= 天数 <= 365):
errors.append({
'病案号': row['病案号'],
'错误类型': '范围校验',
'错误描述': f"住院天数超范围(0-365):{天数}",
'错误等级': '严重'
})

# 总费用
if pd.notna(row.get('总费用')):
费用 = float(row['总费用'])
if 费用 <= 0:
errors.append({
'病案号': row['病案号'],
'错误类型': '范围校验',
'错误描述': f"总费用异常:{费用}",
'错误等级': '严重'
})

return pd.DataFrame(errors)

3.5 校验函数:关联一致性

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
def check_consistency(df):
"""关联一致性校验"""
errors = []

for idx, row in df.iterrows():
# 1. 出院日期 > 入院日期
if pd.notna(row.get('出院日期')) and pd.notna(row.get('入院日期')):
if row['出院日期'] &lt; row['入院日期']:
errors.append({
'病案号': row['病案号'],
'错误类型': '一致性校验',
'错误描述': f"出院日期({row['出院日期']})早于入院日期({row['入院日期']})",
'错误等级': '严重'
})

# 2. 主诊与性别一致性
if pd.notna(row.get('主诊ICD')) and pd.notna(row.get('性别')):
主诊名称 = str(row.get('主诊名称', ''))
性别 = str(row['性别'])

for 关键词, 限制性别 in gender_restricted.items():
if 关键词 in 主诊名称 and 性别 != 限制性别:
errors.append({
'病案号': row['病案号'],
'错误类型': '一致性校验',
'错误描述': f"主诊含[{关键词}]但患者性别为[{性别}]",
'错误等级': '严重'
})
break

return pd.DataFrame(errors)

3.6 校验函数:ICD 编码映射

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
def check_icd_mapping(df):
"""ICD 编码是否合法 + 是否与诊断名称匹配"""
errors = []

for idx, row in df.iterrows():
主诊ICD = str(row.get('主诊ICD', '')).strip()
主诊名称 = str(row.get('主诊名称', '')).strip()

# 1. 是否在 ICD 字典里
if 主诊ICD not in valid_icd:
errors.append({
'病案号': row['病案号'],
'错误类型': 'ICD映射',
'错误描述': f"主诊 ICD 不在字典中:{主诊ICD}",
'错误等级': '严重'
})
continue

# 2. ICD 对应的标准名称 vs 实际诊断名称
标准名称 = icd_dict[icd_dict['ICD编码'] == 主诊ICD]['诊断名称'].values
if len(标准名称) > 0 and 主诊名称:
# 简单匹配:ICD 标准名称的关键词是否在诊断名称里
关键词 = 标准名称[0].replace('其他', '').replace('未特指', '')[:4]
if 关键词 not in 主诊名称:
errors.append({
'病案号': row['病案号'],
'错误类型': 'ICD映射',
'错误描述': f"主诊名称与 ICD 不匹配(ICD={主诊ICD},名称={主诊名称})",
'错误等级': '一般'
})

return pd.DataFrame(errors)

3.7 主函数:把所有校验串起来

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
def validate_首页(文件路径):
"""主函数:运行所有校验,输出错误清单"""
print(f"开始校验:{文件路径}")
df = pd.read_excel(文件路径)
print(f"共 {len(df)} 条首页")

# 跑 5 大校验
错误清单 = pd.concat([
check_required(df),
check_format(df),
check_range(df),
check_consistency(df),
check_icd_mapping(df)
], ignore_index=True)

# 按严重程度排序
等级顺序 = {'严重': 0, '一般': 1, '轻微': 2}
错误清单['排序键'] = 错误清单['错误等级'].map(等级顺序)
错误清单 = 错误清单.sort_values(['排序键', '病案号']).drop(columns=['排序键'])

# 统计
print(f"\n=== 校验结果 ===")
print(f"总错误数:{len(错误清单)}")
print(f"严重错误:{(错误清单['错误等级']=='严重').sum()}")
print(f"一般错误:{(错误清单['错误等级']=='一般').sum()}")

# 按错误类型统计
类型统计 = 错误清单['错误类型'].value_counts()
print(f"\n错误类型分布:")
print(类型统计)

# 保存结果
输出文件 = '首页校验结果.xlsx'
with pd.ExcelWriter(输出文件) as writer:
错误清单.to_excel(writer, sheet_name='错误清单', index=False)
类型统计.reset_index().to_excel(writer, sheet_name='类型统计', index=False)

print(f"\n✓ 结果已保存到 {输出文件}")
return 错误清单

# 运行
错误清单 = validate_首页('本月首页.xlsx')

典型输出:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
开始校验:本月首页.xlsx
共 10250 条首页

=== 校验结果 ===
总错误数:387
严重错误:215
一般错误:172

错误类型分布:
一致性校验 142
ICD映射 118
格式校验 85
必填校验 28
范围校验 14
Name: count, dtype: int64

✓ 结果已保存到 首页校验结果.xlsx

四、避坑指南:5 个让校验脚本「失灵」的常见错误

[!warning] 雷区 ①:只校验主诊,漏了副诊
症状:副诊里有「妊娠合并心脏病」,但 ICD 编码错了。
解决:主诊 + 副诊都要校验,副诊数量最多 10 条。

[!warning] 雷区 ②:ICD 字典版本不一致
症状:医院用的是 2019 版 ICD-10,字典是 2024 版,编码匹配不上
解决:字典版本和 HIS 系统一致,写脚本时注明版本号。

[!warning] 雷区 ③:容错处理缺失,一次报错整个脚本死
症状:一行数据格式异常,整个 iterrows() 崩溃。
解决:try/except 包裹每行校验,异常单独记录。

[!warning] 雷区 ④:校验规则写死,新增规则要改代码
症状:医院新加校验规则,要改 Python 代码,IT 不愿意维护。
解决:把规则配置化,用 Excel/JSON 存规则,脚本读取:

1
2
3
4
rules = pd.read_excel('校验规则.xlsx')
for _, rule in rules.iterrows():
# 动态执行规则
pass

[!warning] 雷区 ⑤:校验完不通知到人
症状:脚本跑完了,结果在 Excel 里,没人看
解决:自动发邮件:

1
2
3
4
5
6
import yagmail
yagmail.SMTP('质管办@xxx.com').send(
to=['编码员@xxx.com'],
subject=f'【首页校验】本月有 {len(错误清单)} 条问题',
contents=['详见附件', '首页校验结果.xlsx']
)

五、把脚本嵌入日常工作流

[!TIP] 实战:每月自动校验

  1. 每月 1 日:脚本自动跑,生成错误清单
  2. 每月 2 日:编码员收到邮件,逐条修改
  3. 每月 3 日:重新校验,确认错误清零
  4. 每月 5 日:汇总错误类型,做培训材料

配置方法:

  • Windows 任务计划程序 → 每月 1 日 02:00 跑脚本
  • 或用 Navicat + 邮件触发器(详见第 09 期)

六、下期预告

[!quote] 第 18 期
Python + DRG 入组分析 —— 让医保结算少扣钱的「代码版」

这一期,带你用 pandas 替代第 10 期的 SQL,做 DRG 入组异常筛查,适合不会 SQL 但会 Python 的医院人。


[!TIP] 互动
如果你觉得这篇「医数智联·第 17 期」对你有用,请点赞、在看、转发给科里的同事。

文中完整校验脚本 + 校验规则 Excel 模板 + ICD 字典示例,我已经整理成「首页自动校验工具包」。点击「阅读原文」可直接下载

我是白衣狼,一个用数字化工具把医院管理做「轻」的实战派。咱们下期见。