news 2026/9/16 8:09:59

eICU没有ventilation_event?手把手重建机械通气事件

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
eICU没有ventilation_event?手把手重建机械通气事件

最近在跑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的表设计更“扁平化”:患者表、生命体征表、呼吸治疗表、用药表各管一摊。呼吸机相关的数据没有单独的“事件表”,核心存在respiratoryCarerespiratoryCharting两张表里。

这就导致了一个结果:在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呼吸治疗记录唯一IDjoin时保持唯一性
respOffset距离ICU入室的时间偏移,单位分钟eICU的时间轴统一用offset表示,0表示入ICU时刻
airwayType当前气道装置类型判断有创/无创机械通气的重要字段,比如OralEndotracheal、Tracheostomy等
ventilatorMode呼吸机模式文本值比较杂,需要做模式清洗和归一化
FiO2吸入氧浓度单位不统一,有21/0.21两种写法,需要预处理
tidalVolumeSet设定潮气量呼吸机参数,有可能为空
tidalVolumeObserved实测潮气量更接近真实通气执行情况
setPEEP设定PEEP算呼吸支持强度时很有用
O2AdminDevice氧疗装置用于区分氧气面罩、鼻导管等非通气状态

一个常见的误区是看到ventilatorMode非空就认为患者“正在机械通气”。从实际数据看,respiratoryCare里也会记录一些辅助氧疗或气道护理的条目,所以严谨一点的做法是结合airwayTypeventilatorMode一起判断。比如模式为CPAP但气道类型为鼻罩或面罩,这通常是无创通气;而有气管插管/气管切开气道记录且模式非空,则基本可以认定是有创通气。

2.2 respiratoryCharting表的“纵向存储”玩法

respiratoryCharting让我一开始很不适应。它的每一行只存储一个指标的“键-值”对,比如某个时间点的observationNamePIPobservationValue28,下一行可能又是set PEEP,值变成5。也就是说,我们习惯的“宽表”(一行一个时刻、多列不同指标)在这里被拆成了长表。

这种结构处理起来其实也不难,核心方法是按observationName做行转列,或者用条件聚合把目标指标提取出来。我实际处理时经常提取的指标有:

  • FiO2(吸氧浓度)
  • Tidal Volume (set)Tidal Volume (observed)
  • set PEEPmeasured PEEP
  • Respiratory 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表的搭配关系

做通气事件重建时,respiratoryCarerespiratoryCharting是主力,但还经常需要patient表来对时间轴做锚定。patient表里有unitAdmitOffsetunitDischargeOffsethospitalAdmitOffset等字段,它们定义了一个患者的时间零点。eICU里的所有xxxOffset都是相对ICU入室时间的分钟数,所以如果你要换算成绝对时间或者按天划分,必须关联patient表获得unitAdmitOffsetunitDischargeOffset

vitalPeriodicvitalAperiodic也有呼吸频率、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;

需要注意,observationValuerespiratoryCharting里经常是字符串类型,直接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导致记录膨胀,时长翻倍

算总通气时长时,如果我把respiratoryCarerespiratoryCharting直接join完再按患者分组求和,会因为一对多关系导致记录膨胀,时长虚高。正确顺序是:先在子查询里把respiratoryChartingpatientunitstayid, respOffset聚合到目标指标,再和阶段表关联。做任何“每个患者XX小时”的统计前,先看一眼中间结果的行数是否符合常识。

6. 一点个人经验和额外建议

如果让我给刚接触eICU的人一个建议,那就是:先把respiratoryCare表的ventilatorModeairwayType做成两个维度交叉表,看看实际数据分布长什么样,再开始写分析SQL。这一步能帮你快速建立对数据质量的直觉,也能发现一些异常编码。

另外,建议把整个“重建通气事件”的SQL整理成一个可复用的视图或临时表。因为后续做统计、画图、建模都会反复用到这个基础表,一次写好,后面会很省心。

我个人的体会是,eICU里“没有ventilation_event”不是坏事,它逼着你把自己的分析口径显式化,反而让结果更透明。如果你是从MIMIC转过来的,心态上要完成一个切换:不是“我要找到那张表”,而是“我要用原始记录把事件定义出来”。这个过程最初有点磨人,但跑通之后,你对数据的理解会深很多。

希望这篇内容能帮你少踩几个坑,顺顺利利把通气事件表建出来。如果你也遇到过其他奇奇怪怪的eICU数据问题,欢迎在评论区聊聊,我大概率也踩过。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/16 8:09:04

Agent技能体系化设计:从Function Calling到可复用技能系统

1. 这不是一个工具&#xff0c;而是一套组织Agent能力的思维方式这半年只要你在搞LLM应用&#xff0c;就绕不开一个词&#xff1a;Agent。但真正上手做过的朋友应该都有体会&#xff0c;做个能聊天的Demo容易&#xff0c;做一只能“干活”的Agent难。难在哪儿&#xff1f;难在怎…

作者头像 李华
网站建设 2026/9/16 8:08:02

小程序番茄钟状态机实现与生命周期适配

简介&#xff1a;这是一份面向小程序初学者与时间管理类应用开发者的番茄工作法实践源码&#xff0c;提供开箱即用的微信小程序完整实现方案。资源包含核心功能模块&#xff1a;任务计时、专注/休息状态切换、历史记录查看、界面动效&#xff08;GIF演示&#xff09;及响应式UI…

作者头像 李华
网站建设 2026/9/16 8:06:53

Jetson边缘AI开发能力地图:从驱动安装到工业部署

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/16 8:06:39

DeskcommCRM:桌面端通信集成客户管理系统的设计与实践

做客户管理这块这么多年&#xff0c;我越来越觉得&#xff0c;很多团队缺的不是销售能力&#xff0c;而是一套能让信息真正转起来的工具。DeskcommCRM这个项目&#xff0c;就是冲着这个去的——它在名字里已经把三件事讲清楚了&#xff1a;Desk&#xff08;桌面工作台&#xff…

作者头像 李华
网站建设 2026/9/16 8:06:33

TCAN4550系统基础芯片实战:从硬件设计到CAN FD采样点配置

TCAN4550RGYRQ1这颗料&#xff0c;我在不少汽车电子项目里都遇到过&#xff0c;也亲手调过几轮。今天这篇就从这颗系统基础芯片&#xff08;SBC&#xff09;本身出发&#xff0c;结合我实际调试时的踩坑经历&#xff0c;把TCAN4550从选型、硬件设计、采样点设置到软件驱动完整梳…

作者头像 李华