ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

医疗不良事件分析报告设计:数据模型、指标口径与自动化生成

医疗不良事件分析报告设计:数据模型、指标口径与自动化生成

医疗数据里最难做的一类报表,不是门诊量,也不是收入结构,而是不良事件分析报告。它牵扯的字段散落在上报系统、住院病案首页、护理记录、药品与器械台账、科室组织架构里,口径一旦含糊,做出来的数字谁都不敢拿去开会用。这篇就聊医疗数据之不良事件分析报告的设计与实现——数据模型怎么拆、指标口径怎么锁、风险分级怎么落地、模板引擎怎么选、定时任务怎么兜底,把能直接抄的结构和踩过的坑都摊开讲。适合正在做医疗质量数据、院内报表平台、质控分析模块的开发和数据岗,也适合临床科室里每月要手工整理上报数据、被表格折磨到深夜的质控联络员。读完你至少能干三件事:把事件表设计成能扛住下钻的结构、把七个核心指标的口径一次锁死、把从取数到出 PDF 的链路自动跑起来。

1. 这类报告为什么值得重新设计

1.1 人工拼装报告的四类典型短板

先说清楚问题在哪,不然设计出来的东西容易变成"把 Excel 搬到网页上"。不良事件报告在不少机构里的现状是:每月初,质控岗从上报系统导一份明细,从病案系统导一份出院人次,从护理部要一份压疮与跌倒的分级清单,然后在表格软件里用透视表拼出几张图,最后贴进文字模板。这套流程能跑,但短板非常明显。

第一是口径漂移。这个月按"发生例数"算,下个月有人按"上报条数"算,中间还夹着一条事件被两个科室分别上报的重复。数字一改,同比环比全废,会上被追问一句"上个月是 37 例这个月怎么变 29 例"就答不上来。第二是分母不一致。事件发生率的分母到底是出院人次、住院日数还是占用床位数,不同科室各用各的,横向排名直接失去意义。

第三是不可回溯。报告里写"跌倒事件环比上升 12%",但点不进去看是哪些科室、哪些班次、哪些年龄段,结论只能停在描述层,落不到整改。第四是时效性差。人工链路通常要三到五个工作日,等报告出来,问题已经过去半个月,整改窗口早就关了。

注意:不要一上来就追求"大屏可视化"。不良事件报告的第一读者是质控会和科室主任,他们要的是能签字的结论和能追责的明细,不是一个会转的圆环图。

1.2 报告的定位与读者分层

设计之前必须把读者分层,因为不同人看报告的深度完全不同。我把读者分成三层:决策层看首页概览,只关心总量、趋势、风险最高的三个科室和本期的严重事件;管理层的质控与护理部看科室排名、类型分布、整改闭环率,需要下钻到科室维度;执行层的科室质控员看自己科室的个案清单、每一条事件的等级判定和整改状态。

这三层对应三种粒度:汇总层、科室层、个案层。数据模型必须同时支撑这三种粒度,否则后面加下钻功能就得重构。我的做法是在事件主表上保留完整维度字段(科室、病区、事件类型、等级、发生时段、报告人角色),汇总表只作为性能优化,不作为唯一数据源。这样任何一个汇总数字都能反查到明细,会上被质疑时能当场打开个案列表。

还有一点容易被忽略:报告要写明数据截止时间和数据来源系统。这不是形式主义,是防止"两份报告数字不一样"的扯皮。我习惯在报告首页右上角固定一行小字:数据截止 2024-05-31 24:00,来源为上报系统与病案首页,口径版本 v2.3。口径版本号这个习惯救过我很多次,改口径时留痕,历史报告不会被追溯修改。

2. 数据模型与指标口径:先把地基打对

2.1 事件主表、维度表与分母表的三角结构

整个报告的数据底座我一般拆成三块:事实表存事件,维度表存字典与组织架构,分母表存住院日与出院人次。三块缺一不可,很多人只做前两块,结果每次算率都要临时去连业务库,慢且不稳。

事实表的核心字段我列在下面,字段名按常见习惯命名,具体实现按你的库调整:

字段类型说明
event_idvarchar事件唯一号,建议用业务单号而非自增
patient_keyvarchar患者脱敏标识,用于同人合并
age / genderint / char年龄与性别,年龄建议同时存年龄段冗余字段
dept_code / ward_codevarchar发生科室与病区,取事件发生地而非上报人所在科室
event_type_codevarchar事件类型,走字典表
harm_leveltinyint伤害分级 1-4
sac_severitychar严重度 S1-S4
sac_likelihoodchar可能性 A-D
occur_timedatetime发生时间
discover_timedatetime发现时间
report_timedatetime上报时间
preventabilitytinyint可预防性 0 不可预防 / 1 可预防 / 9 待评
outcomevarchar转归,字典化
statustinyint待处理 / 处理中 / 已结案
rectify_done_timedatetime整改完成时间

这里有几个设计决定值得解释。为什么存发生科室而不是上报科室:上报人可能是夜班护士代报别的病区,按上报科室统计会让排名失真。为什么把 age 和年龄段都存:年龄段的分组边界会随业务调整,存冗余字段可以在不改历史数据的前提下重算分布。为什么 harm_level 和 sac_severity 分开:前者是监管口径的四级分类(警告事件、不良后果事件、未造成后果事件、隐患事件),后者是机构内部风险评估用的矩阵维度,两者用途不同,强行合并以后一定要拆。

分母表相对简单,但坑最多。它至少要有:科室编码、统计月份、出院人次、实际占用总床日数、床位周转次数。床日数建议直接取病案首页或住院日报的汇总,不要用"出院人次 × 平均住院日"倒推,倒推出来的数字和财务口径永远对不上。

2.2 七个必算指标与口径锁死方法

报告里的指标不怕少,就怕口径飘。我通常固定七个,每个都写清楚分子分母和取值时点:

指标计算方式取值时点与备注
报告率(每百出院人次)事件例数 ÷ 同期出院人次 × 100例数按 event_id 去重后计
千日发生率事件例数 ÷ 同期实际占用床日 × 1000适合压疮、跌倒、管路类长期观察项
严重事件占比Ⅰ级 + Ⅱ级事件数 ÷ 事件总数 × 100%监管口径的四级分类
及时上报率24 小时内上报数 ÷ 事件总数 × 100%以 report_time − occur_time 计
可预防事件占比preventability=1 的数量 ÷ 已评估总数分母只算已评估,不算"待评"
整改闭环率已结案且整改完成 ÷ 应结案数应结案口径要按事件等级分别定义时限
同类事件重复率同科室同类型 90 天内出现 ≥2 次的事件数 ÷ 该类事件总数用于识别系统性缺陷

口径锁死的关键动作有三个。第一,把公式写进配置文件,不要散落在代码里。我习惯用一个metrics.yaml存公式、分子分母来源表、过滤条件,代码只负责执行。第二,除零保护统一处理:分母为 0 时输出—而不是 0,因为 0 会被下游误读为"没有发生"。第三,同比环比的基期必须显式声明,尤其是组织架构调整过的科室,基期要按新的科室映射回溯重算,否则新科室的同比一定是无穷大。

举一个真实会遇到的细节:及时上报率里,"24 小时"从哪个时间点起算?从 occur_time 还是 discover_time?这两个口径能差出十几个百分点。业内的常见做法是以 occur_time 为起点,但隐患事件(Ⅳ级)往往没有明确的 occur_time,这时以 discover_time 兜底,并在报告脚注里注明。这类兜底规则一定要写进文档,不然换个人维护就又是一场口径之争。

2.3 严重度分级与风险矩阵的落地

风险矩阵是不良事件报告里最能体现专业度的一块,也是最容易被做成摆设的一块。基本做法是二维:纵轴严重度,横轴发生可能性,交叉出风险等级。

严重度我按四级定义:S1导致死亡或永久性功能丧失;S2导致严重伤害,需要额外治疗或延长住院;S3造成轻度伤害,需要简单处理;S4未造成伤害或仅为隐患。可能性按同类事件在本机构的发生频次分四档:A频繁(年度内多次)、B偶发、C少见、D罕见。

矩阵落地成一张表更直观:

严重度 \ 可能性A 频繁B 偶发C 少见D 罕见
S1 死亡/永久损伤极高极高高高
S2 严重伤害极高高高中
S3 轻度伤害高中中低
S4 无伤害/隐患中中低低

实现上不要让人工填风险等级,而是让人工填 S 和 A-D,等级由矩阵自动算。这样做的好处是评级标准统一,且矩阵可以随机构策略整体调整,改一次配置全量重算。可能性那一维建议按"同科室同类型事件近 12 个月的发生次数"自动分档,减少主观性:≥4 次记 A,2-3 次记 B,1 次记 C,0 次记 D。这条自动分档规则要在报告的方法说明里写出来,让读者知道数字怎么来的。

提示:矩阵本身没有绝对正确的版本,关键是全院统一、年度稳定。中途改矩阵会让历史报告的风险分布失真,改之前先做一版新旧对照。

3. ETL 与数据质控:指标可信的前提

3.1 接入与清洗的关键规则

事件数据接进来第一件事不是算指标,而是清洗。常见的脏数据有四类:时间倒挂、字典外码值、科室编码失效、必填字段缺失。

时间倒挂最典型的是discover_time < occur_time,上报人填错或者系统默认值导致。处理方式不能简单删掉,我的做法是标记time_flag=1并在计算及时上报率时排除,同时在质控清单里列出,让科室回修。字典外码值通常是新增事件类型没同步字典,处理成event_type_code='UNKNOWN'并在报告里单列"其他/未分类",绝不能悄悄归到某个已有类目里,那是最隐蔽的口径污染。

科室编码失效多见于组织架构调整,老科室撤并后编码在字典表里查不到。处理方式是维护一张科室映射表,把历史编码映射到当前编码,映射关系带生效时间区间。这张表看着麻烦,但没有它,跨年度的同比根本做不了。

必填缺失的判断要按事件等级区别对待。Ⅰ级、Ⅱ级事件的患者标识、发生时间、伤害描述必须齐全;Ⅳ级隐患事件允许部分字段为空。一刀切地要求全字段完整,会导致大量隐患事件被挡在统计之外,报告的灵敏度反而下降。

-- 事件清洗与标准化(示例,按实际表名调整) WITH raw AS ( SELECT event_id, patient_key, dept_code, event_type_code, harm_level, occur_time, discover_time, report_time, preventability, status FROM ods_event_report WHERE stat_month = '${biz_month}' ), mapped AS ( SELECT r.*, COALESCE(d.dept_code_std, r.dept_code) AS dept_std, CASE WHEN r.event_type_code IN (SELECT code FROM dim_event_type) THEN r.event_type_code ELSE 'UNKNOWN' END AS type_std, CASE WHEN r.discover_time < r.occur_time THEN 1 ELSE 0 END AS time_flag FROM raw r LEFT JOIN dim_dept_mapping d ON r.dept_code = d.dept_code_old AND r.occur_time >= d.valid_from AND r.occur_time < d.valid_to ) SELECT * FROM mapped;

3.2 去重、归并与时间窗对齐

去重是不良事件统计里最容易被低估的环节。同一个患者在同一次住院期间,跌倒可能被护理系统报一次、被科室口头报一次、被家属投诉转成一次,三条记录描述同一件事。直接计数会虚高,虚高的数字在排名里就是冤枉人。

我的规则是:同一 patient_key + 同一 dept_std + 同一 type_std,且在 48 小时窗口内,合并为一条,保留最早发生时间和最严重的那条等级。48 小时这个窗口是经验值,不同事件类型可以不同——跌倒、给药错误用 24 小时更合适,压疮这类持续状态可以放宽到 7 天。窗口参数放在配置里,别写死。

合并之后还要处理"升级"的情况。一条隐患事件(Ⅳ级)后续造成实际伤害,会被追改成Ⅱ级。这时候要保留原始记录的审计痕迹,报告里按最终等级统计,但趋势分析里要看"等级变更率",这个比率高说明初始分级能力有问题,是质控的另一个抓手。

时间窗对齐主要影响分母。事件按 occur_time 归月,分母按统计月份归月,两者必须同源。我见过一个实现是事件按上报月份归集、分母按出院月份归集,结果每个月的事件数都像是"上个月的尾巴",趋势图永远是斜的。统一原则:事件按发生时间归期,分母按同期业务量归期,跨月事件在报告脚注注明。

3.3 自动化质控清单

数据质量不能靠人眼抽查,得有一份能自动跑的检查清单,每次出报告前跑一遍,不通过就阻断。

检查项判定规则处理动作
时间倒挂discover_time < occur_time 或 report_time < occur_time标记并排除出及时率计算
字典外码值事件类型/科室编码不在有效字典内归入未分类并告警
分母为空某科室当月出院人次为 0 但事件数 > 0阻断,人工核对科室映射
等级缺失harm_level 为空标记待补,暂按Ⅳ级处理并留痕
重复上报同人同科室同类型 48 小时内多条自动合并,保留最早记录
结案超时已过整改时限但 status 未结案计入闭环率分母,报告单列
总量突变本月事件数与近 6 个月均值偏离超 ±50%告警,人工确认后再发布

最后一条特别有用。总量突变通常是数据源出了问题,比如上报系统某天接口断了,或者某科室批量补录。有这条告警,就不会出现"本月事件数下降 60%,全院质量显著提升"这种自欺欺人的结论。

4. 报告生成的工程实现

4.1 技术选型:三条路线怎么选

报告生成本质是"数据 + 模板 → 文档",路线有三条,各有适用场景。

方案技术栈优势代价适用场景
HTML 模板渲染后转 PDFJinja2 + HTML/CSS + WeasyPrint 或浏览器打印排版自由度高,图表可嵌 SVG,样式好调分页控制需要调 CSS,中文字体要单独配追求版式统一的正式月报
Word 模板填充python-docx 或模板占位符替换临床和行政熟悉 Word,便于二次编辑复杂表格和图表插入麻烦,样式容易跑版需要人工批注、走签批流程
BI 工具看板报表平台拖拽上线快,支持下钻和交互导出正式文档的排版能力弱,口径分散在多个数据集日常监控,不适合作为归档报告

我的组合是:HTML 渲染出 PDF 作为正式归档报告,同时把指标结果写入汇总表供看板读取。这样既保证了归档文档的版式稳定,又保留了交互查询的能力。纯 Word 方案我也用过,适合需要科室签字回传的场景,但每次改模板都要重新对齐占位符,维护成本比想象中高。

字体是 PDF 方案的第一个坑。中文字体必须显式指定,否则渲染出来全是方块。做法是在 CSS 里用@font-face引入本地字体文件,并在渲染前确认字体已注册:

# 渲染 PDF 的关键片段(示例) from weasyprint import HTML, CSS base_css = CSS(string=""" @font-face { font-family: 'ReportSans'; src: url('file:///opt/report/fonts/SourceHanSansCN-Regular.otf'); } body { font-family: 'ReportSans', sans-serif; font-size: 10.5pt; } table { border-collapse: collapse; width: 100%; } thead { display: table-header-group; } /* 跨页时表头重复 */ tr { page-break-inside: avoid; } /* 防止行被切断 */ """) html = render_template("monthly_report.html", ctx=context) HTML(string=html, base_url="/opt/report/").write_pdf( "/data/report/2024-05_不良事件分析报告.pdf", stylesheets=[base_css] )

thead { display: table-header-group }这一行很关键,长表格跨页时表头会自动重复,否则第二页开始就没有列名,读者对不上号。

4.2 指标计算的核心代码

指标计算建议单独抽成一个模块,输入是标准化后的事件 DataFrame 和分母 DataFrame,输出是一张宽表。这样做的好处是计算逻辑与渲染逻辑解耦,同一套计算可以喂给 PDF、看板和接口。

import pandas as pd import numpy as np def calc_dept_metrics(events: pd.DataFrame, base: pd.DataFrame, dept_col: str = "dept_std") -> pd.DataFrame: """ events: 已清洗去重的事件明细 base: 分母表,含 dept_std, stat_month, discharge_cnt, bed_days """ ev = events.copy() ev["is_severe"] = ev["harm_level"].isin([1, 2]).astype(int) ev["is_timely"] = ( (ev["report_time"] - ev["occur_time"]).dt.total_seconds() <= 24 * 3600 ).astype(int) ev["is_preventable"] = (ev["preventability"] == 1).astype(int) ev["is_evaluated"] = ev["preventability"].isin([0, 1]).astype(int) ev["is_closed"] = (ev["status"] == 2).astype(int) agg = ev.groupby([dept_col, "stat_month"]).agg( event_cnt=("event_id", "nunique"), severe_cnt=("is_severe", "sum"), timely_cnt=("is_timely", "sum"), preventable_cnt=("is_preventable", "sum"), evaluated_cnt=("is_evaluated", "sum"), closed_cnt=("is_closed", "sum"), ).reset_index() df = base.merge(agg, on=[dept_col, "stat_month"], how="left").fillna(0) # 除零保护:分母为 0 输出 NaN,渲染层显示为 "—" df["rate_per_100_discharge"] = np.where( df["discharge_cnt"] > 0, df["event_cnt"] / df["discharge_cnt"] * 100, np.nan) df["rate_per_1000_beddays"] = np.where( df["bed_days"] > 0, df["event_cnt"] / df["bed_days"] * 1000, np.nan) df["severe_ratio"] = np.where( df["event_cnt"] > 0, df["severe_cnt"] / df["event_cnt"], np.nan) df["timely_ratio"] = np.where( df["event_cnt"] > 0, df["timely_cnt"] / df["event_cnt"], np.nan) df["preventable_ratio"] = np.where( df["evaluated_cnt"] > 0, df["preventable_cnt"] / df["evaluated_cnt"], np.nan) df["closure_ratio"] = np.where( df["event_cnt"] > 0, df["closed_cnt"] / df["event_cnt"], np.nan) return df.round(4)

几个细节值得说。nunique而不是count,因为去重可能不彻底,用唯一号更稳。fillna(0)放在合并之后,保证没有事件的科室也出现在结果里,报告中的科室排名需要"零事件科室"作为对照。最后统一round(4)在计算层做,渲染层只管格式化,避免两处舍入规则不一致。

4.3 图表规范与排版细节

图表不是越花越好。不良事件报告的图表我固定用五种:月度趋势折线(带近 6 个月移动平均)、事件类型横向条形图、科室发生率排名条形图、风险矩阵热力图、严重事件个案时间轴。五种图对应五类问题,读者看图的目的是找异常,不是欣赏配色。

配色上遵守两条:同一种事件类型在所有图表里用同一个颜色,避免读者重新建立映射;严重度用渐变色(浅到深表示 S4 到 S1),不用红绿对比,因为色觉差异人群分辨不了红绿。

趋势图有个必做动作:叠加移动平均线。单月数据受节假日、上报习惯影响波动很大,只有折线会让读者误判。三个月移动平均能滤掉大部分噪音,报告里同时给出原始值和移动平均值,读者自己判断。

排版上,我把报告固定成六页左右的结构:第一页概览(总量、趋势、TOP3 风险科室、严重事件提示),第二页指标表(全院与重点科室对照),第三页分布(类型、时段、人群),第四页科室排名与下钻指引,第五页个案清单(严重事件与重复事件),第六页整改闭环与方法说明。这个结构稳定下来之后,读者每期翻到固定位置找固定信息,阅读效率会明显提升。

# 图表输出为 SVG 嵌入 HTML,避免位图放大模糊 import matplotlib matplotlib.use("Agg") import matplotlib.pyplot as plt plt.rcParams["font.sans-serif"] = ["Source Han Sans CN"] plt.rcParams["axes.unicode_minus"] = False fig, ax = plt.subplots(figsize=(7.2, 2.8), dpi=150) ax.plot(df["stat_month"], df["event_cnt"], marker="o", label="月度事件数") ax.plot(df["stat_month"], df["event_cnt"].rolling(3).mean(), linestyle="--", label="3个月移动平均") ax.set_ylabel("事件数(例)") ax.legend(frameon=False) ax.spines[["top", "right"]].set_visible(False) fig.tight_layout() fig.savefig("/data/report/fig/trend.svg", format="svg")

中文乱码的根因几乎都是字体没注册。除了rcParams,还要确认系统里确实装了对应字体,容器镜像里经常缺。我一般把字体文件打进镜像,用fc-list验证一遍再跑。

4.4 调度、分发与幂等

自动化报告最怕两件事:跑重了和跑漏了。跑重会导致同一期报告发两遍,科室收到两份数字略有差异的 PDF,信任度直接归零;跑漏更糟,月中开会发现没有数据。

调度用 cron 就够,关键是加三重保护。第一,任务幂等:以"报告期 + 版本号"作为输出文件名,重跑覆盖同名文件而不是追加,同时在汇总表里用report_period做主键 upsert。第二,数据就绪检查:任务启动先查分母表和事件表的目标期数据量,为 0 或明显低于阈值就退出并告警,不生成半成品。第三,生成后校验:PDF 页数、关键指标非空、总事件数与源表一致,三项过了才允许分发。

# 每月 5 日 02:00 生成上月报告;失败重试 3 次 0 2 5 * * /opt/report/bin/run_report.sh --period $(date -d "last month" +\%Y-\%m) >> /var/log/report.log 2>&1

分发渠道看机构习惯,邮件、内部消息、共享目录都行。我的建议是报告正文只放结论和目录,明细走附件或链接,邮件正文塞满表格会导致移动端完全没法看。附件命名统一成YYYY-MM_不良事件分析报告_v2.3.pdf,版本号跟着口径走,方便归档检索。

5. 报告呈现与解读:让临床愿意看下去

5.1 首页概览的信息密度控制

首页决定读者会不会翻第二页。信息太少显得空洞,太多又抓不住重点。我的首页固定四个模块:本期核心指标卡(事件总数、千日发生率、严重事件数、闭环率,各带同比箭头)、12 个月趋势图、风险最高的三个科室及主要事件类型、本期需重点关注的严重事件条目(最多三条,超出转到明细页)。

指标卡的同比箭头要谨慎。事件数上升不一定是坏事,可能是上报积极性提高,这恰恰是质量文化改善的信号。所以我在箭头旁边加一行小字说明:"报告率上升需结合上报及时率与重复率综合判断,不宜直接解读为质量下降。"这种提示看似啰嗦,但能避免会上出现"你们科事件最多,是不是管理最差"的误伤,也保护了上报积极性。

5.2 科室下钻与个案回溯

科室页的核心不是排名,而是可解释的差异。同样是跌倒发生率高,一个是老年科、一个是外科,人群结构完全不同,直接排名没有意义。我的做法是在科室排名表里加两列:科室的患者年龄结构与平均住院日,读者自己就能看出结构差异。进一步的做法是做标准化发生率,按年龄和住院日做间接标准化,但这一步对数据要求高,样本量小的科室结果不稳定,我通常只在全院层面用,科室层面保留原始率加结构说明。

个案回溯是报告的价值锚点。每条严重事件要能点开看到:事件描述、时间线(发生—发现—上报—处理—结案)、等级判定依据、整改措施与责任人角色、是否重复发生。这条链路打通了,报告才从"统计表"变成"管理工具"。实现上不需要复杂的前端,PDF 里放事件编号,配套一个内部查询页面按编号检索即可。

5.3 结论页怎么写才不像套话

结论页最容易写成"加强管理、提高认识、持续改进"三件套,这种东西写一百遍也没人看。有效的结论必须满足三条:指向具体科室或具体环节、有数据支撑、有可执行的下一步动作和时限。

举个写法对比。差的写法是"跌倒事件需引起重视,加强防范"。好的写法是"跌倒事件本期 14 例,其中 9 例发生在 22:00 至次日 06:00,占 64%,集中在两个病区的三人间;建议这两个病区在两周内完成夜间巡视频次调整与床栏使用核查,下期报告跟踪该时段占比是否降至 50% 以下"。后者有分布、有归因假设、有动作、有可验证的指标,这才是分析报告应有的样子。

6. 常见问题与排查速查

6.1 数据类问题的定位思路

指标突然翻倍或腰斩,先查三处:数据源是否重复导入(看event_id是否有重复批次)、科室映射是否失效(失效会导致事件全归到"未分类")、分母表是否缺月(缺月直接导致率虚高)。这三处覆盖了八成异常。

科室排名反常,八成是分子分母不同源。典型情况是事件按发生科室归集,分母按出院科室归集,转科患者会被两边各算一次。解决办法是统一按发生地归集,或者在分母里按床日拆分到科室,做之前先确认业务上认可哪种。

同比出现极端值,先看基期。新设科室、合并科室、口径变更三种情况都会导致基期不可比,处理方式是标注"基期不可比"并给出绝对值,不要硬算百分比。

6.2 生成类问题的处理

PDF 中文变方块:字体未注册,检查@font-face路径与容器内字体文件是否存在,用fc-list | grep -i han确认。

长表格跨页断行:加tr { page-break-inside: avoid },同时给表格设table-layout: fixed并指定列宽,否则内容会撑破版心。

图表模糊:改用 SVG 或把 dpi 提到 150 以上,位图在 PDF 里缩放后必然失真。

报告生成成功但邮件没发出去:检查附件大小限制,PDF 里嵌了大量位图时很容易超过 10MB,建议图表统一走 SVG,能把体积压到 2MB 以内。

6.3 速查表与几条避坑心得

现象可能原因排查动作
千日发生率异常高分母床日数缺失或单位错(人日 vs 床日)对比病案首页月报的床日数
及时上报率接近 100%上报时间被系统默认值覆盖抽查 20 条记录核对 report_time
某类型事件为 0字典未同步新增类型查字典表与源表码值差集
闭环率虚高分母只算了已结案分母改为"应结案"并按等级设时限
报告页数与预期不符明细行数突增导致分页溢出明细页设上限,超出改写"共 N 条,详见附件"
同一期两次生成结果不同中间有补录数据,任务非幂等加数据快照时间戳,固定取数时点

几条实操心得。取数时点要固定,我在任务启动时先给源表打一个快照时间戳,后续所有查询都带这个时间戳过滤,避免跑批过程中有新数据进来导致前后不一致。口径版本号要进文件名,改口径不覆盖历史报告,新旧并存一段时间再下线。报告要能被质疑,每个汇总数字旁边留一个"口径说明"的脚注编号,会上被问到时能立刻翻到对应说明,比当场解释有说服力得多。

还有一条:不要在报告里放太多"自动结论"。系统自动生成的文字很容易写成正确的废话,比如"本月事件数较上月上升"。真正有用的结论一定是人写的,数据系统负责把事实摆清楚、把异常标出来,判断留给质控人员。我在实现里只做异常标记和阈值告警,结论段落保留人工撰写入口,这个分工用下来最稳妥。

如果后续要扩展,我会优先做两件事:一是把事件与整改措施做成结构化关联,能统计"哪类整改措施对哪类事件真正有效",这比单纯的发生率更有管理价值;二是把科室维度的数据做成季度对比,月度数据噪音太大,季度视角更适合看趋势。这两块都不难,难的是先把前面那套口径和质控规则稳下来,地基不牢,加什么功能都是白搭。

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进