最近在跑eICU数据库做重症数据分析,遇到一个让我卡了很久的问题:想直接查询机械通气的相关事件,脑子里习惯性地敲了ventilation_event,结果表不存在。一开始我还以为自己下错版本、导错文件了,反复确认了好几遍,最后才反应过来——eICU的数据模型里压根就没有这张表。
这个事说起来是个小坎,但对从MIMIC系列数据库转过来的研究者来说特别容易踩。大家都习惯了MIMIC里现成的ventilation_event事件表,到了eICU一查没有,第一反应往往是“我是不是少导了数据”。其实不是,eICU把呼吸机相关的信息拆到了另外几张表里,数据是有的,只是没有帮你拼成一张“事件表”。这篇文章我就想聊聊:eICU里呼吸机数据到底藏在哪里、怎么把通气事件自己重建出来、以及我在实操中踩过的那些坑。
写这篇东西的目标读者,是正准备用eICU做重症研究、尤其是需要做机械通气相关分析的同学。如果你只是想跑一个“通气天数”的统计,或者想复现某篇论文里“首次机械通气时间”的变量,这篇文章能帮你省下大把翻文档和试错的时间。
1. 为什么eICU里没有ventilation_event
1.1 MIMIC和eICU的建模思路差异
先说说为什么两个库会有这种差别。MIMIC系列的ventilation_event表其实并不是从原始系统里直接导出来的,它是数据团队在整理期间,把呼吸机模式、参数设置、撤机等状态变化整理成了一条条事件记录。MIMIC的哲学偏向“研究者友好”,很多脏活累活他们已经帮你干了一部分。
eICU不一样。eICU Collaborative Research Database的定位更像“面向临床大数据挖掘的原始数据仓库”,它把电子病历系统里的记录尽量按原始结构存下来,让你自己去组合。所以你会发现eICU的表设计更“扁平化”:患者表、生命体征表、呼吸治疗表、用药表各管一摊。呼吸机相关的数据没有单独的“事件表”,核心存在respiratoryCare和respiratoryCharting两张表里。
这就导致了一个结果:在MIMIC里你一条SQL就能拿到通气开始结束时间,在eICU里你得先搞清楚这两张表怎么组织、字段什么含义,然后写一段不算短的处理逻辑自己造一张事件表出来。
1.2 呼吸机数据到底藏在哪张表里
eICU中与呼吸支持、机械通气相关的数据,主要分布在两张表里。
一张是respiratoryCare。这张表可以理解为“呼吸治疗方案记录”,记录了患者接受呼吸道管理或呼吸支持时的设置和装置信息。常见字段包括ventilatorMode(呼吸机模式)、airwayType(气道类型,比如经口气管插管、气管切开等)、FiO2(吸入氧浓度)、tidalVolumeSet(设定潮气量)、setPEEP(设定PEEP)等。每一行可以理解成一次评估或参数变更记录。
另一张是respiratoryCharting。这张表存储的是呼吸治疗相关的连续监护或评估数据,比如某个时刻测到的平台压、峰压、实际呼出潮气量、SpO2等。它是一个典型的“长表”结构,observationName字段表示指标名称,observationValue字段存的是具体数值或文本。
所以从eICU做通气分析的正确姿势是:以respiratoryCare作为事件骨架,再从respiratoryCharting里按时间匹配抓取你需要的细化参数。千万不要上来就到处找现成的“event表”。
2. 两张核心表的字段细节拆解
2.1 respiratoryCare表的关键字段说明
我先把我最常用的字段列出来,并说明它们在分析里的用途。这里的字段名和eICU官方schema一致,你可以直接在自己库里验证。
| 字段名 | 含义 | 分析用途与注意点 |
|---|---|---|
patientunitstayid | 单次ICU住院的唯一ID | 所有时间相关计算都要按它分组,不能直接用uniquepid(同一患者可能多次进ICU) |
respiratoryCareID | 呼吸治疗记录唯一ID | join时保持唯一性 |
respOffset | 距离ICU入室的时间偏移,单位分钟 | eICU的时间轴统一用offset表示,0表示入ICU时刻 |
airwayType | 当前气道装置类型 | 判断有创/无创机械通气的重要字段,比如OralEndotracheal、Tracheostomy等 |
ventilatorMode | 呼吸机模式文本 | 值比较杂,需要做模式清洗和归一化 |
FiO2 | 吸入氧浓度 | 单位不统一,有21/0.21两种写法,需要预处理 |
tidalVolumeSet | 设定潮气量 | 呼吸机参数,有可能为空 |
tidalVolumeObserved | 实测潮气量 | 更接近真实通气执行情况 |
setPEEP | 设定PEEP | 算呼吸支持强度时很有用 |
O2AdminDevice | 氧疗装置 | 用于区分氧气面罩、鼻导管等非通气状态 |
一个常见的误区是看到ventilatorMode非空就认为患者“正在机械通气”。从实际数据看,respiratoryCare里也会记录一些辅助氧疗或气道护理的条目,所以严谨一点的做法是结合airwayType和ventilatorMode一起判断。比如模式为CPAP但气道类型为鼻罩或面罩,这通常是无创通气;而有气管插管/气管切开气道记录且模式非空,则基本可以认定是有创通气。
2.2 respiratoryCharting表的“纵向存储”玩法
respiratoryCharting让我一开始很不适应。它的每一行只存储一个指标的“键-值”对,比如某个时间点的observationName是PIP,observationValue是28,下一行可能又是set PEEP,值变成5。也就是说,我们习惯的“宽表”(一行一个时刻、多列不同指标)在这里被拆成了长表。
这种结构处理起来其实也不难,核心方法是按observationName做行转列,或者用条件聚合把目标指标提取出来。我实际处理时经常提取的指标有:
FiO2(吸氧浓度)Tidal Volume (set)和Tidal Volume (observed)set PEEP、measured PEEPRespiratory Rate (set)、Respiratory Rate (observed)PIP(吸气峰压)Plateau Pressure(平台压)Pressure Support(压力支持)Flow Rate(流速)
这里有个特别需要注意的地方:不同医院、不同呼吸机厂商的记录文本并不完全一致。同样是PEEP,有的写set PEEP,有的写PEEP,有的干脆写成PEEP SETTING。所以直接用WHERE observationName = 'PEEP'会漏掉大量记录。建议先对observationName做一次DISTINCT查询,把实际出现的值拉出来看一遍,再建一个“同义词映射表”,把相近的指标归并成统一标准。
2.3 与patient表、vitalPeriodic表的搭配关系
做通气事件重建时,respiratoryCare和respiratoryCharting是主力,但还经常需要patient表来对时间轴做锚定。patient表里有unitAdmitOffset、unitDischargeOffset、hospitalAdmitOffset等字段,它们定义了一个患者的时间零点。eICU里的所有xxxOffset都是相对ICU入室时间的分钟数,所以如果你要换算成绝对时间或者按天划分,必须关联patient表获得unitAdmitOffset和unitDischargeOffset。
vitalPeriodic和vitalAperiodic也有呼吸频率、SpO2等指标,但我在重建通气事件时一般不用它们做主线,只用于校验。比如我看某个阶段respiratoryCare有通气模式,但vitalPeriodic里的呼吸频率长时间为0,那就可能不是真的在通气,而是记录有误或患者已经撤机但设置没更新。
3. 在eICU中重建呼吸机事件的完整方案
3.1 先定义什么叫“通气事件”
动手写SQL之前,必须先定义清楚分析口径。在实际分析里,“通气事件”有两种粒度。
第一种是“记录级事件”,也就是每次呼吸机设置变更、评估的一条respiratoryCare记录。这种粒度适合做参数趋势分析,比如观察PEEP或FiO2在一段时间里的变化轨迹。
第二种是“治疗阶段事件”,也就是患者从开始通气到撤机之间的一段连续治疗期。论文里的“机械通气持续时间”“首次机械通气时间”通常指的就是这个阶段级的定义,它需要你把多条记录拼接成段。
我通常用下面的规则判定“正在机械通气”:
ventilatorMode字段非空,且respOffset字段有效(非空且在ICU住院时间范围内)
如果有创/无创的区分需求,就再加一条:
airwayType包含OralEndotracheal、NasalEndotracheal、Tracheostomy等有创气道设备时,判为有创通气;- 模式为CPAP、NIPPV、BiPAP等且无有创气道记录时,判为无创通气。
这里别指望eICU官方给你一个现成的“是否机械通气”布尔值,他们没给。自己构造一个清晰的判定规则,反而更可控,也更方便写进论文的方法学部分。
3.2 怎么把散记录拼接成连续通气阶段
有了判定规则,下一步就是把满足条件的记录变成“阶段”。我用的是相邻记录间隔判断法:同一患者的两条有效记录,如果时间间隔小于某个阈值,就认为它们属于同一个通气阶段;如果间隔太大,就认为是撤机后又重新上机,新起一个阶段。
阈值取多少需要根据临床场景来定。我看文献和实际探索中,常见的选择是2小时(120分钟)或4小时(240分钟)。如果阈值太小,容易把一个连续阶段拆得零碎;如果阈值太大,又会把两段独立通气合并成一段。没有绝对的对错,关键是定完之后要写清楚,并且做敏感性分析。我一般先按120分钟跑一版,再用240分钟跑一版对比,看主要结论是否稳健。
3.3 核心SQL实操:生成患者的通气阶段表
下面这段SQL是我在实际分析里用过的核心逻辑,把零散的respiratoryCare记录转化成带phase_id的通气阶段。
-- Step 1: 筛选有效通气记录,并获取前一条记录的时间偏移 WITH vent_rec AS ( SELECT patientunitstayid, respOffset, ventilatorMode, FiO2, setPEEP, tidalVolumeSet, LAG(respOffset) OVER ( PARTITION BY patientunitstayid ORDER BY respOffset ) AS prev_offset FROM `physionet-data.eicu_crd.respiratoryCare` WHERE ventilatorMode IS NOT NULL AND respOffset IS NOT NULL ), -- Step 2: 判断每条记录是否为一个新通气阶段的开始 vent_phases AS ( SELECT *, CASE WHEN prev_offset IS NULL THEN 1 WHEN (respOffset - prev_offset) > 120 THEN 1 ELSE 0 END AS new_phase FROM vent_rec ) -- Step 3: 用累计求和给每个阶段打上ID SELECT patientunitstayid, phase_id, MIN(respOffset) / 60.0 AS phase_start_hour, MAX(respOffset) / 60.0 AS phase_end_hour, COUNT(*) AS record_cnt FROM ( SELECT *, SUM(new_phase) OVER ( PARTITION BY patientunitstayid ORDER BY respOffset ROWS UNBOUNDED PRECEDING ) AS phase_id FROM vent_phases ) t GROUP BY patientunitstayid, phase_id ORDER BY patientunitstayid, phase_start_hour;这段SQL在BigQuery和PostgreSQL上都能直接跑。输出结果里,每个患者被拆成多段通气阶段,每段有明确的开始小时和结束小时。record_cnt表示这个阶段里有多少条护理记录,如果某些阶段记录特别少,比如只有1条,需要留意是不是噪声数据,可以回去翻原始记录确认。
3.4 用respiratoryCharting补齐参数细节
只有阶段开始和结束时间还不够,做研究时往往还要知道这段时间里患者的吸氧浓度、PEEP等参数水平。这里我用respiratoryCharting来补全细节。
比较稳妥的方式是:先按1小时粒度把通气阶段切成小时段,然后对每个小时段关联最近的respiratoryCharting值。这样做的原因是,respiratoryCharting的记录频率不均匀,有的患者一小时好几条,有的可能两三个小时才一条,直接join容易出现“同一分钟内找不到值”或者“join出一堆重复行”的问题。
WITH hourly_grid AS ( SELECT patientunitstayid, hour_idx, DATETIME_ADD(CAST('2000-01-01' AS DATETIME), INTERVAL hour_idx HOUR) AS dummy_ts FROM ( SELECT patientunitstayid, hours_since_admit AS hour_idx FROM UNNEST(GENERATE_ARRAY(0, 24 * 7 * 10)) AS hours_since_admit ) WHERE ... ), -- 简化示例:直接以respiratoryCare中出现的hour作为网格 vent_hours AS ( SELECT DISTINCT patientunitstayid, CAST(FLOOR(respOffset / 60) AS INT64) AS hour_idx FROM `physionet-data.eicu_crd.respiratoryCare` WHERE ventilatorMode IS NOT NULL ), chart_pivot AS ( SELECT patientunitstayid, respOffset, MAX(CASE WHEN observationName = 'PEEP' THEN CAST(observationValue AS FLOAT64) END) AS peep, MAX(CASE WHEN observationName = 'FiO2' THEN CAST(observationValue AS FLOAT64) END) AS fio2 FROM `physionet-data.eicu_crd.respiratoryCharting` GROUP BY patientunitstayid, respOffset ) SELECT v.patientunitstayid, v.hour_idx, AVG(c.peep) AS avg_peep, AVG(c.fio2) AS avg_fio2 FROM vent_hours v LEFT JOIN chart_pivot c ON v.patientunitstayid = c.patientunitstayid AND c.respOffset >= v.hour_idx * 60 AND c.respOffset < (v.hour_idx + 1) * 60 GROUP BY v.patientunitstayid, v.hour_idx;需要注意,observationValue在respiratoryCharting里经常是字符串类型,直接CAST可能报错,需要先清洗掉"N/A"、空字符串这类异常值。
4. 小时级和天级通气状态的两种聚合方法
4.1 按小时生成通气状态表
有了阶段表之后,很多分析其实都落在“每小时患者是否通气”这个粒度上,比如算呼吸机相关肺炎的暴露时间,或者做时间依赖性协变量。原始接口不建议对respiratoryCare直接用“是否非空”来判断某一小时是否通气,因为呼吸机记录不是每分钟都有,某个小时没有记录不代表没上机。
正确做法是先用阶段表生成“该患者在第几小时处于通气状态”的集合,再去做基于小时的分析。我的做法是先把阶段表拆成小时级状态表:
WITH phases AS ( -- 这里直接复用上面3.3节生成的阶段表 SELECT patientunitstayid, phase_id, MIN(respOffset) / 60.0 AS phase_start_hr, MAX(respOffset) / 60.0 AS phase_end_hr FROM ... GROUP BY patientunitstayid, phase_id ), hours AS ( SELECT patientunitstayid, hour_idx FROM ( SELECT patientunitstayid, CAST(FLOOR(respOffset / 60) AS INT64) AS hour_idx FROM `physionet-data.eicu_crd.respiratoryCare` WHERE ventilatorMode IS NOT NULL ) GROUP BY patientunitstayid, hour_idx ) SELECT h.patientunitstayid, h.hour_idx, MAX(CASE WHEN h.hour_idx >= FLOOR(p.phase_start_hr) AND h.hour_idx < CEIL(p.phase_end_hr) THEN 1 ELSE 0 END) AS on_vent FROM hours h LEFT JOIN phases p ON h.patientunitstayid = p.patientunitstayid GROUP BY h.patientunitstayid, h.hour_idx ORDER BY h.patientunitstayid, h.hour_idx;这样每个患者每小时一行,on_vent为1表示这个小时处于通气阶段,0表示不在。这个表做后续的生存分析、时间依赖性Cox回归都很方便。
4.2 按天聚合计算每日通气时长
另一种常见需求是计算每天的机械通气时长。这里踩过一个坑:直接用阶段开始和结束时间相减得到日时长,看起来没错,但如果你把阶段跨天的情况忽略掉,就会少算很多“跨午夜”的通气时间。
所以我通常会先把阶段展开到分钟级,或者按小时状态表聚合。以下是按小时状态表聚合得到天级通气时长的逻辑:
SELECT patientunitstayid, CAST(FLOOR(hour_idx / 24) AS INT64) AS day_idx, SUM(on_vent) AS vent_hours_per_day, SUM(on_vent) / 24.0 AS vent_day_fraction FROM hours_state GROUP BY patientunitstayid, CAST(FLOOR(hour_idx / 24) AS INT64) ORDER BY patientunitstayid, day_idx;day_idx=0表示ICU入室后第0天(即0到24小时),vent_hours_per_day是当天处于通气状态的小时数,除以24就是“等效通气天数”。如果要求精确到分钟,可以把小时块换成分钟块,但数据量会大很多,实际分析里小时精度通常就够用了。
4.3 首通时间与总通气时长的统计口径
构建好阶段表后,“首次机械通气时间”“总机械通气时长”这类变量非常简单。
- 首次通气开始时间:每个患者取
MIN(phase_start_hr)。 - 总通气时长:每个患者把所有阶段的
(phase_end_hr - phase_start_hr)求和。 - 是否接受过机械通气:只要阶段表里有记录就是1,否则0。
不过要注意,eICU没有官方定义“机械通气”的布尔字段,所以你算出来的“首次机械通气时间”完全取决于你在3.1节定义的规则。不同文章如果规则不同,数值会有差异,这是很正常的。建议在论文方法部分把这个规则写清楚,最好附上SQL仓库地址。
5. 实战高频坑与排查技巧总结
5.1 “有模式文本”不等于“在机械通气”
这个是我最早犯的错。我一开始统计机械通气人群时,直接用了ventilatorMode IS NOT NULL作为筛选条件,结果人数比预想高出一大截。仔细看数据才发现,有些记录虽然模式列有值,比如CPAP,但气道类型是面罩,而且氧合装置是“氧气面罩”而不是呼吸机,这类患者很可能只是无创辅助通气或者甚至只是高流量氧疗的记录。所以,如果研究目标是有创机械通气,建议把airwayType作为额外的纳入条件,不要只看模式文本。
5.2 通气记录频率不均匀,别拿“没有记录”当“没通气”
respiratoryCare不是每分钟都记录的,很多时候护士可能一小时记录一次,甚至更稀疏。如果你直接按原始记录判断“某一小时有记录=在通气,没记录=不在通气”,会严重低估通气持续时间。
解决办法就是用阶段表做状态延续,假设通气状态在两条有效记录之间保持。具体阈值怎么定,可以结合临床合理性和敏感性分析。
5.3 FiO2单位混乱,必须做标准化
FiO2这个字段在eICU里我见过好几种存法:有的存0.21,有的存21,还有的存"21%",甚至有些空值或者"N/A"。统计前必须统一换算成小数或百分数。我习惯统一为小数:
SAFE_CAST(REGEXP_REPLACE(CAST(FiO2 AS STRING), '%', '') AS FLOAT64) / 100.0如果看到FiO2 > 1且值在21-100之间,就再除以100。这类清洗规则我会在SQL里连续判断,避免漏掉异常单位。
5.4 数据缺失不是bug,但会影响阶段识别
有些患者阶段之间只隔了10分钟,却因为护理人员没记录而看起来“断开了”;反过来,有些患者明明已经拔管,但呼吸机模式设置还留在系统里,后面又没更新,导致连续性错误。
我的经验是:造完阶段表后,一定要抽样人工核对。直接按patientunitstayid随机抽20个患者,把他们的原始respiratoryCare记录按时间打出来,手动看一遍阶段切得对不对。这一步不会花太久,但能避免很多低级的系统性错误。
5.5 多表join导致记录膨胀,时长翻倍
算总通气时长时,如果我把respiratoryCare和respiratoryCharting直接join完再按患者分组求和,会因为一对多关系导致记录膨胀,时长虚高。正确顺序是:先在子查询里把respiratoryCharting按patientunitstayid, respOffset聚合到目标指标,再和阶段表关联。做任何“每个患者XX小时”的统计前,先看一眼中间结果的行数是否符合常识。
6. 一点个人经验和额外建议
如果让我给刚接触eICU的人一个建议,那就是:先把respiratoryCare表的ventilatorMode和airwayType做成两个维度交叉表,看看实际数据分布长什么样,再开始写分析SQL。这一步能帮你快速建立对数据质量的直觉,也能发现一些异常编码。
另外,建议把整个“重建通气事件”的SQL整理成一个可复用的视图或临时表。因为后续做统计、画图、建模都会反复用到这个基础表,一次写好,后面会很省心。
我个人的体会是,eICU里“没有ventilation_event”不是坏事,它逼着你把自己的分析口径显式化,反而让结果更透明。如果你是从MIMIC转过来的,心态上要完成一个切换:不是“我要找到那张表”,而是“我要用原始记录把事件定义出来”。这个过程最初有点磨人,但跑通之后,你对数据的理解会深很多。
希望这篇内容能帮你少踩几个坑,顺顺利利把通气事件表建出来。如果你也遇到过其他奇奇怪怪的eICU数据问题,欢迎在评论区聊聊,我大概率也踩过。