上周有个研究生同事跑过来问我:eICU数据库里怎么查不到ventilation_event表?他说自己在MIMIC上做呼吸机相关研究已经写顺了SQL,结果换成eICU后第一步就卡住。我停下手里的活告诉他:你找不到这张表太正常了,eICU的数据模型压根就没打算把呼吸机事件做成一张现成的宽表。这不是bug,是设计。这篇文章就用我自己从MIMIC迁到eICU的踩坑过程,讲讲呼吸机相关数据在eICU里的真实存放位置、字段口径,以及怎么自己拼出一张能用于后续分析的ventilation_event表。不管你是做数据库课程设计、写毕业论文,还是参加重症数据集上的数据竞赛,这一篇应该能帮你少走不少弯路。
1. 从MIMIC跳到eICU,第一件事别找ventilation_event
1.1 一张“不存在”的表背后的数据模型差异
MIMIC系列数据库在社区里的生态非常成熟,很多人做呼吸机时长、机械通气人群筛选时,习惯直接查一张类似ventilation_event的表。这张表本质上是一个后处理产物,把原始表里零散的呼吸机设置、气道压力、通气模式等记录,按时间轴合并成了一段一段的“通气事件”。好用是好用,但也容易让人形成一种惯性:换到另一个数据库,第一反应还是找同名表。
eICU Collaborative Research Database在这一点上完全不同。它发布的是更接近原始采集状态的临床数据库,导出的表结构和MIMIC的预加工风格不一样。eICU没有专门叫ventilation_event的表,也没有直接把“机械通气起止时间”打包成一行行现成数据。呼吸机相关的内容被分散在若干张面向临床记录的表中,需要分析者自己理解语义、自行加工。
这不是eICU的缺陷,而是模型设计取向的问题。eICU更鼓励研究者先理解呼吸治疗的真实记录方式,再按自己的研究定义去构造变量。对于只想快速拿结果的人来说确实多了一步,但对于要严谨分析的人来说,反而给了更多自主空间。
1.2 eICU其实把呼吸机数据拆成了三张细节表
如果你用工具查看eICU的schema,会发现和呼吸机最直接相关的表主要有三张:
- respiratorySupport:记录呼吸支持设备/方式的信息,例如是否有创通气、无创通气、高流量氧疗等。
- respiratoryCharting:记录呼吸治疗单上的参数级charting数据,比如通气模式、潮气量、呼吸频率、PEEP、FiO2。
- respiratoryCare:记录呼吸治疗相关的护理事件或状态变化,例如吸痰、呼吸治疗评估、通气状态变更等。
为了让你快速建立概念,我把三张表的定位整理成下面的对应关系:
| 表名 | 信息颗粒度 | 典型用途 |
|---|---|---|
| respiratorySupport | 支持方式级 | 判断患者接受了哪类呼吸支持,构建通气事件框架 |
| respiratoryCharting | 参数级 | 拿到呼吸机具体参数,验证是否真的处于机械通气状态 |
| respiratoryCare | 事件/状态级 | 查看呼吸治疗干预记录,辅助确认拔管、试脱机等时间节点 |
所以当你寻找ventilation_event表时,真正的答案不是“eICU没有数据”,而是“数据在,但要自己组装”。后面几章我就按实际使用频率,从respiratorySupport开始讲起。
2. 呼吸机事件应该从respiratorySupport里捞:字段与口径
2.1 不要错过非侵入通气:type字段的筛选细节
respiratorySupport是我在eICU中提取呼吸机事件的首选入口。这张表的每一行大致可以理解为一次呼吸支持记录,核心字段通常包括patientunitstayid、呼吸支持类型、记录时间偏移量、以及可能存在的开始/结束时间戳。不同导入版本字段名会有微调,但语义基本一致。
最关键的是类型筛选。很多人图省事,看到“Ventilator”就直接WHERE respSupportType = 'Ventilator',结果把大量BiPAP患者漏掉了。BiPAP在临床上属于无创机械通气,很多研究都要纳入,比如急性呼吸衰竭患者使用无创通气的结局分析。如果你只认Ventilator这个词,样本量会少一截,结论也会偏。
更稳妥的做法是先看一眼这个字段到底有哪些取值:
SELECT respSupportType, COUNT(*) FROM respiratorySupport GROUP BY respSupportType ORDER BY COUNT(*) DESC;跑完这个查询,你就会看到一堆类似“Mechanical Ventilation”“Non-Invasive Ventilation”“BiPAP”“CPAP”“High Flow Nasal Cannula”“Nasal Cannula”之类的值。这时候再根据自己的研究定义筛选。一般来说,如果目标是有创机械通气,保留“Mechanical Ventilation”“Ventilator”“Endotracheal Tube”相关的记录;如果目标包含无创通气,则要把BiPAP、CPAP、Non-Invasive Ventilation一起纳入。
我这里强调一点:不要用一个过于宽泛的ILIKE '%ventilat%'把“High Flow Nasal Cannula”这种加温湿化高流量氧疗设备也算进去。它虽然也有一个看起来像呼吸支持的设备界面,但它不是传统意义上的呼吸机,指标含义和治疗目标差别很大。筛选之前,先把枚举值全部拉出来看一遍,再做决定。
2.2 时间字段用offset还是用datetime:先统一基准
eICU里的时间表达有点特殊。很多表里既有offset字段,也有绝对时间戳字段。respiratorySupport里一般会有一个记录相对偏移量,单位是分钟,从ICU入院时间或者unit admission开始算;同时也可能有类似start/end time的绝对时间字段。
我的建议是:在构建事件表时,优先使用绝对时间字段,因为它可以直接用于和其他表join、画时间轴、算时长。如果你的版本里只有offset,那就要先拿到患者ICU入院时间,再用offset去换算:
-- 用ICU入院时间 + offset分钟数得到一个绝对时间戳 unitadmit_time + offset * INTERVAL '1 minute' AS event_time但要注意,offset的零点到底是“入院时间”还是“入ICU时间”,不同表可能不一样。respiratorySupport一般以unit admission为基准,但我在实际处理中见过有人把offset直接当成小时来用,导致时间全部放大了60倍,最后算出的通气时长根本没法看。
还有一点,绝对时间字段很可能有空值。我在一次分析中发现,部分较早年份的记录只有offset,没有绝对时间戳。所以最好的做法是两列都保留:如果绝对时间存在就用绝对时间,否则用offset换算,然后在结果里加一个time_source字段标记来源。这样后续校验时能快速定位可疑行。
2.3 一条记录可能只代表一个设置快照,而不是一段事件
这是新手最容易误解的地方。很多人默认respiratorySupport里面一行就是一个完整的通气事件,有开始有结束。但实际上,这张表里的记录可能是设备状态变更点,也可能是某个时段的支持方式快照。
比如同一名患者同一天可能连续出现好几行:
| patientunitstayid | respSupportType | 记录时间 |
|---|---|---|
| 12345 | Mechanical Ventilation | 08:00 |
| 12345 | Mechanical Ventilation | 08:15 |
| 12345 | BiPAP | 10:00 |
| 12345 | BiPAP | 12:00 |
如果简单把每行当成独立事件,就会得到多条几乎重叠的“通气事件”。正确做法是先按“患者+通气类型+记录连续性”分组,把中间没有断开的时间段合并成真正的事件段。
具体怎么合,我在第4章会给出SQL示例。这里想强调的认知是:respiratorySupport更像一张状态记录表,而不是一张干净的事件表。把它加工成ventilation_event,正是我们要做的事。
3. 用respiratoryCharting和respiratoryCare给事件表做交叉验证
3.1 respiratoryCharting:参数级证据链
respiratorySupport能告诉你“患者可能在用呼吸机”,但有时候它可能记录了“处方/医嘱”层面的支持方式,并不代表设备真的执行了。为了确认患者确实处于机械通气状态,我会用respiratoryCharting做交叉验证。
respiratoryCharting是呼吸治疗师记录的charting数据,字段一般包括患者id、记录时间、参数名称、参数值、单位。常见参数名称有“Vent Mode”“Set Tidal Volume”“PEEP”“FiO2”“Respiratory Rate”等。如果某段时间在这些参数上都有持续记录,那基本可以实锤患者在呼吸机上。
我在第一次处理时也踩过坑:只根据respiratorySupport判断通气,结果有人明明有记录,但呼吸机参数表里完全没有对应参数。后来发现是设备设置的记录方式不同,有些支持方式只代表“为患者准备了某类设备”,并不代表实际连接。
所以我现在做事件提取时,会额外跑一条参数检查:
SELECT respChartName, COUNT(*) FROM respiratoryCharting WHERE respChartName ILIKE '%mode%' OR respChartName ILIKE '%tidal%' OR respChartName ILIKE '%peep%' OR respChartName ILIKE '%fio2%' GROUP BY respChartName ORDER BY COUNT(*) DESC;如果你发现患者的所有通气时段里,一条呼吸机参数记录都没有,就要回头检查respiratorySupport里的类型筛选是不是太宽了。
3.2 respiratoryCare:护理和呼吸治疗记录中的通气状态
respiratoryCare这张表我一开始是忽略的,后来发现它是很好的辅助证据。里面记录的是呼吸治疗相关的状态和护理动作,比如吸痰操作、气道评估、呼吸治疗意见等。其中有一些字段会明确标记通气状态,例如“Mechanical Ventilation”“Ventilated”等状态字符串。
这类记录本质上来自呼吸治疗师的流程单,和医生的医嘱、设备的自动记录互为补充。它最大的价值在于:可以帮你判断一个事件是“计划内”还是“计划外”。比如拔管、试脱机这种状态变化,往往在respiratoryCare里能看到护理记录,而respiratorySupport不一定有明确标记。
我建议你不要把respiratoryCare当成主表,而是把它当成一个验证表。在构建完ventilation_event后,抽样几名患者,去respiratoryCare里看他们通气时段前后有没有呼吸治疗记录、状态变化是否符合逻辑。如果完全对不上,说明你的事件合并逻辑可能有问题。
3.3 三张表如何串起来用
三张表的共同连接键是patientunitstayid,时间上则可以通过记录时间或offset对齐。我的推荐路径是这样的:
- 先用respiratorySupport搭出候选通气事件的时间框架。
- 再用respiratoryCharting中的通气参数检查事件段内是否有实际参数支撑。
- 最后用respiratoryCare抽查事件边界的临床合理性。
这种“粗筛-细验-抽查”的方式,可以显著减少假阳性。尤其是做多中心数据的时候,不同医院的记录习惯差异很大,A医院可能在respiratorySupport里记得很全,B医院却更依赖respiratoryCharting。只看其中一张表,很容易低估或高估通气流行的比例。
4. 自己拼ventilation_event表:从SQL到结果的完整链路
4.1 先做窄口径:仅保留真正的机械通气记录
下面我给出一个可以直接在PostgreSQL环境里跑通的示例。先假设respiratorySupport包含patientunitstayid、respSupportType、respSupportStartTime、respSupportEndTime这些字段。如果你导入后的字段名不同,先用\d respiratorySupport看一下实际结构,再把SQL里的字段替换掉。
第一步,把候选的通气记录捞出来:
CREATE TABLE vent_raw AS SELECT patientunitstayid, respSupportType, COALESCE(respSupportStartTime, NULL) AS vent_start_time, COALESCE(respSupportEndTime, NULL) AS vent_end_time FROM respiratorySupport WHERE respSupportType IN ( 'Mechanical Ventilation', 'Ventilator', 'BiPAP', 'CPAP', 'Non-Invasive Ventilation' );这一步筛掉的通常是鼻导管、面罩、高流量氧疗等单纯氧疗设备。如果你研究的就是“所有呼吸支持”,那就不需要做这个过滤,直接到下一步合并即可。
4.2 再处理连接和时间重叠:生成连续事件段
第二步是解决重叠和连续问题。我这里用一个常见的相邻记录合并法:把同一患者的记录按开始时间排序,计算当前记录开始时间是否早于前面所有记录的最大结束时间。如果早于,说明它和前一段有重叠或连续,应该归入同一事件组;否则新开一组。
WITH vent_ordered AS ( SELECT *, MAX(vent_end_time) OVER ( PARTITION BY patientunitstayid ORDER BY vent_start_time, vent_end_time ROWS BETWEEN UNBOUNDED PRECEDING AND 1 PRECEDING ) AS prev_max_end FROM vent_raw ), vent_grouped AS ( SELECT *, CASE WHEN vent_start_time <= prev_max_end THEN 0 ELSE 1 END AS is_new_group FROM vent_ordered ), vent_groups AS ( SELECT *, SUM(is_new_group) OVER ( PARTITION BY patientunitstayid ORDER BY vent_start_time, vent_end_time ) AS group_id FROM vent_grouped ) SELECT patientunitstayid, MIN(vent_start_time) AS ventilation_start, MAX(vent_end_time) AS ventilation_end, array_agg(DISTINCT respSupportType) AS vent_types FROM vent_groups GROUP BY patientunitstayid, group_id;运行完这段SQL,得到的就是按患者分组的连续通气事件段。每个group_id代表一段在时间上连续或不重叠的通气时期。如果你只想算总通气时长,就可以直接对这个结果做SUM(ventilation_end - ventilation_start)。
要注意一点:如果某些行的vent_end_time为NULL,需要先处理。常见做法是给一个合理的默认值,比如用开始时间加一个固定时长,或者用同表内下一个记录的开始时间作为结束。到底用哪种,取决于你的研究问题。如果只是算是否使用过呼吸机,那NULL行可以保留但单独标记;如果要算精确时长,NULL就必须谨慎处理,我通常会让NULL结束时间沿用该患者在ICU内的出院时间。
4.3 最后做一致性校验:三个必查项
事件表生成后,不要直接拿去跑统计。先做下面三个校验,能帮你发现大部分低级错误。
第一,检查时间倒挂。正常事件里,ventilation_start必须早于ventilation_end。跑一下:
SELECT COUNT(*) FROM ventilation_event WHERE ventilation_start > ventilation_end;如果数量不为0,说明原始数据里有异常时间戳,或者COALESCE逻辑写错了。
第二,检查事件跨度过长。正常情况下单次通气事件很少连续超过30天,如果出现几百天的记录,大概率是患者信息串了,或者group_id合并时把不同时间段错误拼到了一起。可以把事件时长超过7天的列出来人工看几个。
第三,和respiratoryCharting抽样对比。随机抽20个患者的通气事件,去看看他们的事件时间段内是否有呼吸机参数记录。如果几乎都没有,回到第2章的类型筛选,看看是不是把不该纳入的设备类型算进去了。
我在实际项目中还习惯加一个“最终版”标记字段:在生成事件表时,记录每个事件是由哪些来源合并出来的,遇到争议时能溯源。这个习惯在写论文、应付审稿人补充分析时,帮了我很多次。
5. 踩坑记录与习惯建议:让这个替代方案更可靠
5.1 版本与导入方式导致字段名漂移
eICU的数据版本不是一成不变的。不同年份发布的版本在字段命名上可能有微调,再加上你自己导入数据库时可能统一转小写,或者用工具自动生成建表语句,列名很可能和网上教程不一样。所以,一定要先看导入后的真实schema,不要照着别人的SQL盲跑。
我的习惯是在项目最开始建一个schema_check.sql,把所有和呼吸机相关表的\d结果存下来,同时把每个表的行数、时间段范围都跑一遍。这样一旦后续发现结果异常,可以回去核对是不是版本变了。
5.2 时区、空字符串与NULL的坑
eICU的时间字段大多不强制带时区,直接看是个本地时间,分析时如果和其他数据源合并,最好先在同一个时区基准下统一。有些表里还会出现空字符串而不是NULL,尤其是在文本类型字段中。如果你用WHERE respSupportType != '',可能把空字符串留下;用NULLIF统一处理会更安全。
另外,有些时间字段会被解析成字符串格式,直接进行比较排序会出错。导入后建议第一时间检查字段类型,如果是text/varchar且内容看起来像时间,尽早转成timestamp。
5.3 给后来者的检查清单
最后分享一份我每次处理eICU呼吸机数据都会过的检查清单,你可以直接复制成自己的模板:
- 确认eICU版本,记录respiratorySupport、respiratoryCharting、respiratoryCare三张表的实际字段。
- 拉取respSupportType全量枚举,按研究定义选定纳入类型。
- 检查所有时间字段类型,统一处理空字符串和NULL。
- 用offset换算时间时,确认基准是入院时间还是入ICU时间。
- 生成候选事件后,先做时间倒挂和时长异常检查。
- 抽至少20个患者,用respiratoryCharting人工核对事件真实性。
- 在最终结果中保留来源标记字段,方便回溯。
我自己的习惯是,任何替代表都不能只跑一遍就完事。第一次生成ventilation_event后,我会换一种筛选口径再生成一版,比较两种口径在总人数和总时长上的差异。差异太大就说明中间的某一步有理解偏差,而不是直接选一个“看起来合理”的结果。这种笨办法在数据质量参差不齐的多中心数据库上,反而是最省时间的做法。