eICU没有ventilation_event?手把手重建机械通气事件
发布时间:2026/9/16 8:10:03 锦皓数字建站

最近在跑eICU数据库做重症数据分析遇到一个让我卡了很久的问题想直接查询机械通气的相关事件脑子里习惯性地敲了ventilation_event结果表不存在。一开始我还以为自己下错版本、导错文件了反复确认了好几遍最后才反应过来——eICU的数据模型里压根就没有这张表。这个事说起来是个小坎但对从MIMIC系列数据库转过来的研究者来说特别容易踩。大家都习惯了MIMIC里现成的ventilation_event事件表到了eICU一查没有第一反应往往是“我是不是少导了数据”。其实不是eICU把呼吸机相关的信息拆到了另外几张表里数据是有的只是没有帮你拼成一张“事件表”。这篇文章我就想聊聊eICU里呼吸机数据到底藏在哪里、怎么把通气事件自己重建出来、以及我在实操中踩过的那些坑。写这篇东西的目标读者是正准备用eICU做重症研究、尤其是需要做机械通气相关分析的同学。如果你只是想跑一个“通气天数”的统计或者想复现某篇论文里“首次机械通气时间”的变量这篇文章能帮你省下大把翻文档和试错的时间。1. 为什么eICU里没有ventilation_event1.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同一患者可能多次进ICUrespiratoryCareID呼吸治疗记录唯一IDjoin时保持唯一性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是PIPobservationValue是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_idx0表示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数据问题欢迎在评论区聊聊我大概率也踩过。
锦
锦皓数字建站
深耕本土企业品牌数字化升级,专注原创端正雅致商务官网,从视觉设计到稳定运维全程保驾护航。