信息速览
心电图算法里有一类问题,问的不是「现在是不是房颤」,而是「这场房颤还剩多久」。前者已经被大规模单导联数据集喂得很饱,后者至今没有一个与之相称的公开数据集。原因不难理解:要知道一场房颤什么时候停,就必须守在患者床边一直录,直到它真的停下——而这种等待在临床上既不可控也不便宜。
PhysioNet 在 2004 年用一个很巧的办法绕开了这个成本。它不要求记录覆盖终止的瞬间,而是把一段连续记录切成整分钟,让分钟窗口的末端去逼近终止点:有的窗口之后一分钟房颤才停,有的窗口之后一秒就停,有的窗口之后一小时都还在颤。模型只看到窗内的波形,要判断的是窗外即将发生什么。这就是 AF Termination Challenge Database 的基本构造,也是它二十多年后仍被反复提起的原因。
它的规模很小——80 段一分钟记录,按受试者算学习集只有 20 人;它的标签很粗——只有三组,没有逐条真值,也没有终止时刻标注。它更像一道设计精巧的考题,而不是一座粮仓。若把一个「有干预的房颤终止数据集」的形象套在它身上,从第一个问题开始就会走偏,这一层纠正放在导语里先说清。一屏速览见下表。
| 维度 | 内容 |
|---|---|
| 全名 | AF Termination Challenge Database(也写作 AF Termination Database) |
| 发布方 | PhysioNet(MIT Laboratory for Computational Physiology 维护) |
| 原始数据提供方 | 美国西北大学 Steven Swiryn |
| 首次发布 | 2004 年 4 月 11 日,版本 1.0.0;此后未修订 |
| 记录数 | 80 段(学习集 30 + 测试集 A 30 + 测试集 B 20) |
| 受试者数 | 学习集 20 名;测试集 A 30 名;测试集 B 10 名;三集合互不重叠 |
| 每段时长 | 1 分钟(7680 采样点 ÷ 128 Hz) |
| 通道数 | 2(头文件里两路信号名都写作 ECG) |
| 采样率与存储 | 128 Hz,16 位整数,增益约 140–202 单位/毫伏 |
| 组标签 | N 不终止 / S 一分钟后终止 / T 一秒内终止 |
| 终止方式 | 自发终止(spontaneous),不含电复律、不含药物干预 |
| 标注内容 | 仅 QRS 位置,自动检测、未审核、异位搏动一律标为正常 |
| 文件格式 | WFDB:.hea 头文件 / .dat 信号 / .qrs 标注 |
| 整库体积 | 官方页面写约 3.3 MB(解压后 2.4 MB) |
| 许可 | ODC-BY 1.0,免注册 |
| 关联赛事 | PhysioNet / Computers in Cardiology Challenge 2004 |
| 典型用途 | 终止预测的特征工程基准、节律复杂度的对照实验、教学演示 |
§1 它要解决的具体麻烦
1.1 房颤终止预测是一个"与时间赛跑"的问题
一名房颤患者被送进急诊,心律不齐、心室率偏快,但循环稳定。接下来医生要做一个判断:是等着看它自己会不会转复,还是现在就上药物或电复律。等下去有自发转复的可能,代价是留观时间与床位;立刻干预有成功率高、可控的好处,代价是镇静风险、血栓栓塞风险和被过度治疗的可能。这个决策在指南里有一整套流程,但其中有一块一直是空的:能不能从眼前这段心电图上,看出它离终止还有多远。
这正是 2004 年那届挑战赛提出的问题。官方公告的原话是「预测一次房颤是否会自发终止、或何时终止」。它把问题拆成了两个层次:第一层是二分类,这场房颤会不会停;第二层是三分类,如果会停,是马上停还是再撑一会儿。两层在数据库里对应两个测试集,也对应两个独立的事件。
要理解这个问题的难点,需要先知道房颤的自发终止不是随机的。电生理研究早就发现,终止前的一段时间里,心房激动频率会发生变化,主导频率漂移、组织化程度上升、体表 f 波振幅与周期性出现渐变。这些变化微弱、个体差异大、被 QRS 与 T 波掩盖,用肉眼几乎看不出来,用算法却有可能抓住。挑战赛给了所有人同一批带标签的样本,让不同思路在同一把尺子上比。
1.2 三条常见的错误期待
第一条,期待它是"复律数据库"。 官方用词从头到尾都是 spontaneous termination,自发终止。库内没有电复律记录、没有抗心律失常药物的给药时刻、没有干预前后的对照。把这三类记录混为一谈,会让标签的含义从"预测自然病程"滑向"评估治疗效果",那是另一类数据该做的事。这一点在常见误读表里排在第一位。
第二条,期待它有"终止瞬间"的标注。 恰恰相反,终止时刻被刻意放在记录窗口之外。s 组的终止点落在记录结束后约一分钟,t 组的终止点落在结束后一秒以内(官方原话是在记录结束后一秒之内终止)。所以没有任何一段记录包含"从房颤突然转成窦性"的那一下。想研究转复瞬间的形态变化,这个库给不了。
第三条,期待它是"房颤检测数据集"。 库内每一段都是房颤,没有窦性心律的对照段,也没有非房颤的其他节律。拿它训练"是/不是房颤"的分类器,正样本率百分之百,模型学不到任何东西,只会输出常数。它的任务粒度是"房颤内部的细分",不是"房颤与正常之间"。
这三条期待的共同根源,是把这个库放进了一个它不负责的赛道上。它的定位更窄也更锋利:在房颤之内,判断终止会不会发生。
1.3 它能给到的与给不到的
| 你的需求 | 它给的 | 够不够 |
|---|---|---|
| 房颤终止预测的特征工程基准 | 30 段带组标签的学习集加 50 段测试集,同一套预处理 | 够做小规模对比实验,不足以训练深度模型 |
| 研究终止前的心房电活动特征 | 终止前最长一分钟的双通道波形 | 够,但只有两个通道,且导联位置未被官方说明 |
| 房颤与窦性的二分类 | 无正常节律对照段 | 不够,全部记录都是房颤 |
| 心搏类型分类 | .qrs 标注把异位搏动也标成正常 |
不够,没有类型信息 |
| 复律或药物疗效评估 | 无干预记录 | 不够,性质完全不同 |
| 长时程房颤负荷 | 每段只有一分钟 | 不够,用长时程库 |
| 多中心泛化验证 | 测试集来自不同受试者,但采集机构与设备未分开说明 | 只能算半边,见 §7.4 |
| 教学演示终止预测的三种情形 | 三组定义清晰、每段一分钟、下载极快 | 够,而且是最顺手的入门材料之一 |
1.4 你的处境与它能帮到的事
| 你的处境 | 它能帮你什么 | 下一步该做什么 |
|---|---|---|
| 你在读房颤终止预测的经典论文,想复现其中一两个特征 | 论文用的就是这批记录,组标签与切分方式都能对上 | 先按 §5.2 拿到全部 80 段,再按 s/t 成对关系重建划分;对照原论文的特征定义逐项复算 |
| 你手上有一个房颤检测模型,想扩展到"严重程度"或"转归" | 提供一个从"检测"往"预测"跨一步的最小练习场 | 先用学习集的 30 段跑通三分类,确认掉分幅度在可接受范围,再考虑换更大的私有数据 |
| 你做信号复杂度、熵、主导频率这类无监督指标 | 三组的生理差异恰好落在这些指标的敏感带上 | 用 §5.4 的脚本批量算指标,先看组间是否有区分度,再决定要不要做监督模型 |
| 你要给团队讲清楚"数据泄漏"是什么 | s 与 t 本是同一段连续记录的两半,是最直观的反面教材 | 用 §4.3 的案例演示:按记录乱切时的成绩与按受试者切时的成绩差多少 |
| 你打算做一个房颤终止预测的深度学习模型 | 数据量远远不够,只能当外部小测集 | 去用更大的房颤库做训练,把它留作最后的"换个人群还灵不灵"的检查点 |
| 你在核对某个公开成绩是否能复现 | 挑战赛成绩页给出了分数上限与名次 | 注意满分口径(事件一 30 分、事件二 20 分),不要把分数直接当准确率比 |
1.5 一句话判断
它不是一座更大的房颤库,而是一道出得极好的小考题:用三个人为设定的时间断面,逼着算法去看窗外那一分钟,而这种「看不见终点却要判断终点」的构造,到今天仍然没有第二个公开库照着做。
1.6 不适合谁:四条边界
不适合谁:想用数据量换性能的团队。 80 段、按受试者算学习集 20 人。任何带参数的模型都能在几十步内把学习集拟合到零损失,泛化性能完全取决于那 50 段测试集的表现,而测试集没有逐条真值可供反复调参。
不适合谁:想研究终止瞬间电生理的人。 终止点被设计在窗口之外,这是核心构造而非缺陷。想研究"转复那一秒心房发生了什么",需要的是另一类覆盖终止时刻的连续记录。
不适合谁:把房颤当成单一标签的初学者。 这个库里没有非房颤段。用它入门房颤检测,会得到一个只输出一种结果的模型,而且不会察觉问题出在数据上。
不适合谁:需要干预信息的临床研究者。 没有用药、没有复律能量、没有抗凝状态、没有随访结局。它是一个纯电生理片段库,临床上下文只有"这段来自一位有过自发终止的患者的某一段记录"。
§2 数据构成与规格
2.1 三个目录、五个字母前缀
PhysioNet 的文件目录按集合分了三层,根目录下没有散落记录,而是 learning-set、test-set-a、test-set-b 三个子目录,RECORDS 文件里按相对路径逐行列出全部 80 条记录名。组标签不写在头文件里,而是编码在记录名的首字母:n 开头为不终止组,s 开头为一分钟后终止组,t 开头为一秒内终止组,a 开头属测试集 A,b 开头属测试集 B。
这种"标签藏在名字里"的做法在 2004 年很自然,因为当时还没有统一的元数据表规范。它带来一个具体后果:读取标签时必须解析路径或文件名,不能指望头文件里有一行写着 group。这个习惯如果带到别的库上会失效,反过来,别的库的元数据表在这个库上也不存在。
| 路径 | 条数 | 记录名 | 组标签 | 受试者 |
|---|---|---|---|---|
learning-set/ |
30 | n01–n10、s01–s10、t01–t10 | N / S / T 各十段 | 20 名 |
test-set-a/ |
30 | a01–a30 | 约一半 N、其余 T | 30 名 |
test-set-b/ |
20 | b01–b20 | 每对一段 S、一段 T | 10 名 |
| 合计 | 80 | — | 三分类体系 | 约 60 名(互不重叠) |
上表最后一行的人数需要注意口径:官方分别给出了三个集合的受试者数,并明确三者互不出现,所以按人加总是 20 + 30 + 10 = 60 名。官方没有单独给出"全库共多少名受试者"这句话,这个和是按三段话相加得到的,属于官方陈述的直接推论,不是官方原文。
2.2 字段与文件的完整字典
本库的"字段"分两类:一类是 WFDB 头文件里的关键字行,一类是文件名与路径本身携带的语义。逐项列出如下。
| 信息项 | 所在位置 | 取值形态 | 含义 | 使用注意 |
|---|---|---|---|---|
| 记录名 | .hea 第 1 行首字段 |
n01…b20 |
记录标识 | 文件名前缀即组与集合 |
| 信号条数 | .hea 第 1 行第 2 字段 |
2 |
通道数 | 全部记录均为 2 |
| 采样率 | .hea 第 1 行第 3 字段 |
128 |
每秒采样点数 | 不是 250 也不是 360 |
| 采样点数 | .hea 第 1 行第 4 字段 |
7680 |
总长度 | 7680 ÷ 128 = 60 秒整 |
| 起始时刻 | .hea 第 1 行第 5 字段 |
如 19:32:13 |
该段在原长程记录中的时点 | 学习集与测试集 A 有,测试集 B 没有 |
| 信号文件名 | .hea 信号行第 1 字段 |
n01.dat 等 |
两路信号共用一个文件 | — |
| 存储格式 | .hea 信号行第 2 字段 |
16 |
16 位整数 | 不是浮点 |
| 增益 | .hea 信号行第 3 字段 |
如 166.945/mV |
每毫伏对应的整数个数 | 逐记录、逐通道不同 |
| 基线 | .hea 信号行第 4 字段 |
0 |
ADC 零值 | 换算时先减 |
| 初始值 | .hea 信号行第 5 字段 |
如 -9 |
首采样点整数 | 可用于完整性自查 |
| 校验和 | .hea 信号行第 6 字段 |
如 -17906 |
采样点累加和 | 检测文件是否被改动 |
| 块大小 | .hea 信号行第 7 字段 |
0 |
每帧字节数 | 由 wfdb 自动计算 |
| 信号描述 | .hea 信号行第 8 字段 |
ECG |
两路都写作 ECG | 官方未说明具体导联位置 |
| QRS 位置 | .qrs |
采样点序号 | 自动检测的心搏时刻 | 未审核,含错误 |
| 心搏类型 | .qrs |
一律为正常 | 不含异位类型 | 不能做类型分类 |
| 组标签 | 文件名首字母 | n / s / t / a / b |
终止与否 | a、b 只表示集合不含组信息 |
| 集合归属 | 路径 | 三个子目录名 | 学习或测试 | 测试集无逐条真值 |
增益这一列值得单独说一句。头文件里写的是形如 166.945/mV 的表达式,意思是每毫伏 166.945 个整数单位。抽样看到的记录里,增益大致落在每毫伏 140 到 202 个单位的区间,两路信号常常不同。这意味着同一个毫伏值在两路里对应的整数不同,直接拿整数当特征会把通道间的幅度关系搞乱。
2.3 一条记录的完整解剖
以 learning-set/t01 为例。头文件第一行是:
t01 2 128 7680 19:33:14
读法是:记录名 t01,两路信号,128 Hz 采样,共 7680 个采样点,起始时刻 19 点 33 分 14 秒。7680 ÷ 128 恰好等于 60,所以每段都是精确的一分钟,切窗时不需要处理零头。
接下来两行声明信号:
t01.dat 16 166.945/mV 0 0 38 -19233 0 ECG
t01.dat 16 173.01/mV 0 0 -162 14341 0 ECG
两行末端的描述都是 ECG,没有写 II、V1 这类导联名,也没有任何注释行给出电极位置。这是一个必须记住的事实:官方从头到尾没有说明这两个通道取自哪个导联位置。文献与教程里偶有"近似 II 与 V1"的说法,但官方页面与头文件都不支持这个对应,涉及导联特异性的结论在这批数据上根本无法成立——你既不知道通道对应哪里,也就没有资格说"这个特征在 V1 上更明显"。务必把两路信号当作不带解剖含义的"通道一"与"通道二"来用。
再看同一来源的另一条 learning-set/s01:
s01 2 128 7680 19:32:13
19:32:13 与 t01 的 19:33:14 正好相差 61 秒。这不是巧合:官方明确说 t 组记录就是 s 组记录的接续,t01 紧跟在 s01 之后。61 秒而不是 60 秒,说明两段之间有一秒左右的空档,这与官方对测试集 B 成对记录"之间可能有不足一秒的短间隔"的描述是同一类现象。
读取信号与标注的标准写法:
import wfdb
#-- 直读 PhysioNet 上的库,不需要先下载
base = 'aftdb/1.0.0/learning-set'
rec = wfdb.rdrecord('t01', pn_dir=base)
ann = wfdb.rdann('t01', 'qrs', pn_dir=base)
print(rec.fs, rec.sig_len, rec.sig_name) #-- 128 7680 ['ECG', 'ECG']
print(rec.p_signal.shape) #-- (7680, 2),已换算成毫伏
print(len(ann.sample), ann.symbol[:5]) #-- 心搏条数与前五个类型(全为 N)
p_signal 是已经按增益与零值换算好的浮点毫伏值,d_signal 则是原始整数。ann.symbol 里只会出现一种符号,这正是 §2.2 里说的"异位搏动也标为正常"的直接后果。
2.4 三组的构造关系
理解这个库最关键的一张表,不是记录清单,而是三组之间的时间关系:
| 组 | 记录名 | 终止点相对记录末端 | 原话依据 | 可否与其他组配对 |
|---|---|---|---|---|
| N | n01–n10 | 长程记录结束前一小时以上都未观察到终止 | 官方:at least an hour following the segment | 不配对,各自独立 |
| S | s01–s10 | 记录结束后约一分钟 | 官方:terminates one minute after the end of the record | 与同号的 t 配对 |
| T | t01–t10 | 记录结束后一秒以内 | 官方:terminates immediately (within one second) | 与同号的 s 配对 |
| A | a01–a30 | 约一半 N、其余 T,官方未给逐条标签 | 官方:approximately half … belong to group N | 每条独立,一受试者一段 |
| B | b01–b20 | 每对一段 S、一段 T | 官方:one record of each pair belongs to group S | 两两成对,共十对 |
这张表里最容易被忽略的是 N 组的定义括号:「在其后的至少一小时里都未观察到终止」。这句话的判据是"没看到终止",不是"这段房颤注定不终止"。两者的差别在统计上很重要:N 组是一个"至少在一小时内没有终止"的集合,其中可能混着后来终止了的病例,只是终止发生在观察窗之外。用这个库训练出的模型,学到的是"这一小时里没停"的先验,不是"永远不会停"的确定性。
S 与 T 的关系则是另一种:它们本来就是同一段连续记录被切开的两半。t01 紧接在 s01 之后,两者首尾相衔,合起来覆盖终止前约两分钟。这个构造让 s 与 t 之间存在极强的时间连续性,而这一点正是 §4.3 的案例要展开的。
2.5 与同类资源的字段差异
| 对比项 | AF Termination | MIT-BIH 房颤库 | 长期房颤库 | PAF 预测挑战赛库 | CinC 2017 |
|---|---|---|---|---|---|
| 记录数 | 80 段 | 25 段 | 84 段 | 100 段 | 8528 段 |
| 单段时长 | 1 分钟 | 10 小时 | 24–25 小时 | 30 分钟 | 9–61 秒 |
| 采样率 | 128 Hz | 250 Hz | 128 Hz | 128 Hz | 300 Hz 或 500 Hz |
| 通道数 | 2 | 2 | 2 | 2 | 1 |
| 任务 | 房颤内部终止预测 | 房颤检测与节律标注 | 房颤负荷 | 房颤发作预测 | 房颤检测 |
| 节律标注 | 无(只有三组标签) | 有(逐段节律) | 有 | 有(发作起点) | 有(四分类) |
| 心搏标注 | 自动、未审核、单一类型 | 人工审核、多类型 | 人工审核、多类型 | 自动与人工混合 | 无 |
| 许可 | ODC-BY 1.0 | ODC-BY 1.0 | ODC-BY 1.0 | ODC-BY 1.0 | ODC-BY 1.0 |
| 训练可用性 | 极低 | 中 | 中 | 中 | 高 |
这张表要说明的只有一件事:在"房颤"这个大类下,五个库分属五条完全不同的任务线。它们之间不存在"哪个更好"的问题,只存在"你的任务落在哪一行"的问题。AF Termination 是唯一把"终止"当作预测目标的那一行。
2.6 一分钟窗口的物理含义
7680 个采样点在 128 Hz 下是一分钟,这个长度不是随手定的。它对应临床上一段心电图打印纸的长度(25 mm/s 走纸时约 25 厘米),也对应一个医生能一眼看完、又不至于丢失节律趋势的尺度。对算法来说,一分钟大约包含 60 到 150 个心搏,取决于心室率;对房颤这种 RR 间期高度不规则的节律,这个心搏数足够估计 RR 序列的统计量,也足够做几轮频谱分析。
但它也有明确的短板。房颤终止前的电生理变化,文献里描述的时间尺度从十几秒到几分钟不等。一分钟窗口能覆盖"立刻就要停"的那类变化,对"还有五分钟才停"就力不从心。这解释了为什么 s 组(一分钟后终止)的预测难度明显高于 t 组(一秒内终止),也解释了为什么两个挑战赛事件的分数上限不同。
| 窗口内的心搏数(估计) | 对应心室率 | RR 序列可估的统计量 | 一分钟内是否足够 |
|---|---|---|---|
| 约 60 | 60 次/分 | 均值、标准差、粗略熵 | 够观察趋势,不够做稳定的谱估计 |
| 约 90–110 | 90–110 次/分 | 加上 LF/HF 粗分 | 最常见区间,足够 |
| 约 140–150 | 140–150 次/分 | 谱估计方差偏大 | 勉强够 |
上表的心搏数是按心室率反推的算术结果,不是从数据里统计出来的。它只用来帮助判断"一分钟够不够",不代表本库各组的实际心室率分布——那个分布需要逐段计算,超出了本篇的范围。
§3 它要回答的那个电生理问题
3.1 贡献者与来历
数据库的作者是 PhysioNet 的 George Moody,原始记录由美国西北大学(Northwestern University)的 Steven Swiryn 提供。PhysioNet 在致谢里写的原话是「特别感谢西北大学的 Steven Swiryn 提供了在此摘录的记录」。这句话框定了数据的性质:这是一批既有的长程记录被摘录,而不是为新数据集专门采集的样本。
这个来历解释了很多细节。为什么每段正好一分钟?因为从 20–24 小时的长程记录里摘一分钟是最省事的切法。为什么只有两个通道?因为原始长程记录本身就是两通道 Holter 式记录。为什么没有年龄、性别、用药?因为这些元数据在摘录时没有一并移交。数据的形态完全由"摘录"这个动作决定,而不是由某个采集方案决定。
3.2 时间线
| 时间 | 事件 | 意义 |
|---|---|---|
| 2003-10-08 | 挑战赛宣布启动,数据集同时可用 | 库的最初发布形态 |
| 2004-04-11 | 项目页标注的发布日,版本 1.0.0 | 官方版本号的起点 |
| 2004-05-01 | 官方公告称已有二十位参赛者提交了首次结果 | 赛事进行中 |
| 2004-09-14 | 官方公告里提到的最终提交截止日 | 评分口关闭 |
| 2004-10-31 | 最终成绩发布 | 成绩页与获胜软件一并公布 |
| 其后至今 | 版本停在 1.0.0,无修订 | 二十余年未变 |
上表后三行来自挑战赛页面的公告段落,属于官方陈述;最后一行是核对页面版本号得到的观察。整个库从 2004 年至今没有第二个版本,这一点在引用时很有用:不存在"新旧版本混用"的问题,任何一次下载得到的都是同一批数据。
3.3 为什么用"摘录"而不是"采集"
如果今天要重新做这件事,最常见的做法是建一个前瞻性队列,招募房颤患者,连续监护直到转复,然后把终止前后的完整记录公开。这种做法能得到终止瞬间、能得到临床变量、能控制样本量。2004 年没有这么做,原因至少有两条。
第一是隐私与伦理的成本。长程记录的终止意味着患者在医院或家中被持续监护,这段记录里必然包含大量与房颤无关的私人信息——睡眠、活动、服药时刻。要公开,就需要做大量脱敏,而当年的脱敏工具与流程远不如今天成熟。
第二是时间成本。前瞻性队列要等到足够多的自发终止事件才能凑够样本,而自发终止本身不是每天都会发生的事。摘录既有记录则可以在几周内完成。
这两条理由共同塑造了这个库的性格:它的样本是"恰好被录到并且被存下来"的那批,而不是"按设计被招进来"的那批。这种非设计性的采样方式,是它所有局限的源头(§7.3)。
3.4 数据来源的三个层次
理解这份数据要分清三层:长程记录(原始,未公开)、一分钟段(公开的基本单位)、QRS 标注(公开的唯一监督信号)。第一层不可得,意味着无法重建 s 与 t 之外的时间上下文,也无法知道 N 组那些记录后来到底终止了没有;第二层决定了每段都是人为切的,切点位置决定了它能提供什么信息;第三层决定了它能支持的监督任务只有"心搏在哪里",而不包括"心搏是什么类型"。
§4 它最独特的东西:把终止点推到窗外
4.1 时间断面式的标注为什么罕见
绝大多数心电数据集的标注是"描述性的":这段里有几个早搏、这段是房颤还是窦性、这个导联有没有 ST 改变。描述性标注的共同点是,被标注的对象就在记录内部,标注者看到什么就写什么。
AF Termination 的标注是"关系性的":它描述的不是记录内部的内容,而是记录与外部事件的时间关系。s 组的记录本身看起来就是一段普通的房颤,没有任何内部标记说明"一分钟后要停"。要判断它属于哪一组,必须知道窗外发生了什么。这种标注方式在公开数据集里极其罕见,因为它要求提供方掌握记录之外的随访信息。
这也是它最难被复制的地方。想再造一个类似的库,你不仅需要房颤记录,还需要知道每一段之后发生了什么——而这恰恰是自我监护记录里最不容易拿到的部分。
4.2 三种时间断面各自考验什么
三个组看起来只是终止时间的差别,实际上考验的是三种不同的能力:
| 组 | 时间断面 | 考验的能力 | 可利用的线索 | 难度 |
|---|---|---|---|---|
| T | 一秒内终止 | 捕捉终止前最后阶段的电生理征兆 | f 波频率与组织化的急剧变化 | 挑战赛里有队伍拿到满分,说明线索确实存在 |
| S | 一分钟后终止 | 判断趋势,而不只是瞬时状态 | 较长时间窗内的频率漂移与振幅衰减 | 明显更难,满分上限也更高(30 分对 20 分) |
| N | 一小时内未终止 | 排除假阳性 | 缺乏上述征兆 | 作为负类,其难度体现在不被误判 |
第三行值得展开。N 组在挑战赛里不是独立事件,而是作为 T 组的负类出现:事件一要在 30 段里挑出哪些属于 T,其余是 N。所以 N 组的作用是提供"看起来像但不会停"的干扰项。一个只学会"房颤就是不终止"的模型,在事件一上会得到接近一半的正确率,这个基线是理解任何成绩的起点。
4.3 案例:s01 与 t01,同一场房颤的两半
这是全篇反复要回到的一个具体例子,因为它把这个库最要紧的两个性质同时展示出来了。
先看时间戳。s01 的起始时刻是 19:32:13,t01 是 19:33:14,相差 61 秒。s01 长 60 秒,所以它覆盖 19:32:13 到 19:33:13;t01 覆盖 19:33:14 到 19:34:14。两者之间空了大约一秒。官方对这个空档没有解释,只说 t 组记录"紧跟" s 组记录。测试集 B 的成对记录也有类似情况,官方在那里明确说"某些成对记录之间存在不足一秒的短间隔"。
再看增益。s01 的两路增益是 166.945 与 173.01,t01 的两路增益完全相同,都是 166.945 与 173.01。而同一目录下的 n01 增益是 142.045 与 143.062,完全不同的一对数。增益一致意味着这两段来自同一次记录、同一个放大器设置,这是文件层面的直接证据,比时间戳更硬。
import wfdb
for name in ('s01', 't01', 'n01'):
h = wfdb.rdheader(name, pn_dir='aftdb/1.0.0/learning-set')
print(name, h.base_time, [g for g in h.adc_gain])
这段代码输出了三行,前两行的增益向量相同、时间戳相差 61 秒,第三行两者都不同。把 s01 与 t01 分到训练集和测试集两侧,就等于把同一场房颤的前后两分钟分别给了模型和考官。 模型在训练时见过的那段波形,与测试时看到的那段在噪声、电极接触、f 波形态上高度相关。这种泄漏比"同一个患者的不同记录"更严重,因为它连"不同的时间窗"这个缓冲都没有。
4.4 标注的粒度:只有位置,没有类型
.qrs 文件是本库唯一的逐事件标注。官方对它的描述有三层限定,每一层都削减了它的用途:
第一层,「由自动检测器产生」。这意味着它没有经过人工复核,误检与漏检都可能存在。官方原话承认这些标注"可能包含少量错误"。
第二层,「所有检出的心搏,包括任何异位搏动,都被标为正常」。这直接否定了它做类型分类的可能。房颤患者常见室性早搏与差异性传导,这些在 .qrs 里与正常心搏没有区别。
第三层,「未经审核」。官方给了三个选项:修正它、在未修正的形态下使用并研究对少量检测错误稳健的方法、或者完全忽略它自己做检测。这三个选项本身就说明了官方对它的定位——一个方便,不是一个保证。
| 用途 | 能否直接用 .qrs |
原因 |
|---|---|---|
| 划分 RR 序列 | 可以,但要容忍少量错误 | 位置基本可用 |
| 计算心率 | 可以 | 对偶发误检不敏感 |
| 心搏类型分类 | 不可以 | 类型信息被抹平 |
| 早搏负荷统计 | 不可以 | 异位搏动被标为正常 |
| QT 间期测量 | 不建议 | 位置未校正,且无导联信息 |
4.5 边界条件:什么情况下它不再适用
| 条件 | 是否适用 | 判断依据 |
|---|---|---|
| 你要研究自发终止前的电生理 | 适用,这是它唯一的设计目标 | 三组定义 |
| 你要研究电复律或药物转复 | 不适用 | 官方全文用 spontaneous |
| 你要做房颤检测 | 不适用 | 全部记录都是房颤 |
| 你要做心搏分类 | 不适用 | .qrs 无类型 |
| 你要训练深度模型 | 不适用 | 80 段、20 人 |
| 你要做外部小测集 | 适用,前提是能自己重建标签 | 学习集有标签,测试集没有 |
| 你要做多导联研究 | 有限适用 | 只有两路,且导联位置未说明 |
| 你要做节律机制研究 | 适用,但需自行提取 f 波 | 有原始波形,但无心房标注 |
这张表的第三、四行是最常被踩的两行。它们的共同点是"把这个库当成一个通用房颤库",而它实际上是一个任务极窄的库。把它放在正确的赛道上,它很好用;放在错误的赛道上,它会在第一次训练就暴露出问题。
§5 怎么拿到:从目录树到可训练张量
5.1 目录结构与下载面
官方页面给的下载选项有三条:ZIP 整包、wget 递归、AWS 命令行同步。文件体积很小,官方写占用约 3.3 MB,页面另一处写解压后总计 2.4 MB。这两个口径的差异官方没有说明,推测一个是含压缩头与目录的开销、一个是纯文件内容量,但不必深究——全量下载在任何网络环境下都是几秒钟的事。
| 路径 | 内容 | 大小量级 |
|---|---|---|
RECORDS |
80 行相对路径 | 约 1.2 KB |
ANNOTATORS |
一行注释角色说明 | 31 字节 |
SHA256SUMS.txt |
全部文件的校验和 | 约 20 KB |
learning-set/*.hea |
头文件 | 每条数百字节 |
learning-set/*.dat |
两路信号 | 每条约 30 KB |
learning-set/*.qrs |
QRS 标注 | 每条数千字节 |
test-set-a/、test-set-b/ |
同构 | 同上 |
ANNOTATORS 文件的内容是 qrs unaudited beat annotations,只有这一行。这行字与本篇反复强调的判断一致:官方自己在文件层面就把这份标注定性为"未经审核"。
5.2 批量下载
#-- 方式一:整包下载后解压
curl -L -o aftdb.zip https://physionet.org/static/published-projects/aftdb/aftdb-1.0.0.zip
unzip -q aftdb.zip -d aftdb
#-- 方式二:按 RECORDS 逐条取(适合只想拿学习集)
mkdir -p aftdb_ls
curl -sL https://physionet.org/files/aftdb/1.0.0/RECORDS -o RECORDS
grep '^learning-set/' RECORDS | while read r; do
for ext in hea dat qrs; do
curl -sL "https://physionet.org/files/aftdb/1.0.0/${r}.${ext}" -o "aftdb_ls/$(basename $r).${ext}"
done
done
ls aftdb_ls | wc -l #-- 预期 90,即 30 条记录各三个文件
第二种方式里要注意路径:RECORDS 里写的是 learning-set/n01 这样的相对路径,下载 URL 必须原样拼接,但在本地存盘时要去掉目录层级,否则 wfdb 读取时会找不到。
5.3 用 wfdb 直接流式读取
不想下载全量时,可以让 wfdb 直接从 PhysioNet 读。这在只做几十段实验时最省事:
import wfdb
sets = {'learning-set': [f'{p}{i:02d}' for p in 'nst' for i in range(1, 11)],
'test-set-a': [f'a{i:02d}' for i in range(1, 31)],
'test-set-b': [f'b{i:02d}' for i in range(1, 21)]}
count = 0
for sub, names in sets.items():
for n in names:
rec = wfdb.rdrecord(n, pn_dir=f'aftdb/1.0.0/{sub}')
assert rec.fs == 128 and rec.sig_len == 7680 and rec.n_sig == 2
count += 1
print('read', count, 'records')
这段代码做了一件有用的事:它用断言把 §2.2 里那三个规格数字变成可执行的检查。如果哪天官方换了版本、采样率或长度变了,这段代码会立刻报错,而不是悄悄读进形状不对的张量。这种断言在长期维护的实验仓库里值得保留。
5.4 转成通用的表结构
WFDB 格式不适合直接喂给深度学习框架,通常要先落成 NumPy 或 Parquet。这里给一个最小的转换脚本,把每条记录存成(7680, 2)的浮点数组,并把组标签写在文件名里:
import os
import wfdb
import numpy as np
OUT = 'aftdb_npz'
os.makedirs(OUT, exist_ok=True)
labels = {'n': 'N', 's': 'S', 't': 'T', 'a': 'A', 'b': 'B'}
names = [f'{p}{i:02d}' for p in 'nst' for i in range(1, 11)]
names += [f'a{i:02d}' for i in range(1, 31)]
names += [f'b{i:02d}' for i in range(1, 21)]
for n in names:
sub = 'learning-set' if n[0] in 'nst' else ('test-set-a' if n[0] == 'a' else 'test-set-b')
rec = wfdb.rdrecord(n, pn_dir=f'aftdb/1.0.0/{sub}', physical=True)
x = rec.p_signal.astype('float32') #-- (7680, 2),单位毫伏
np.savez_compressed(os.path.join(OUT, f'{n}.npz'), x=x,
label=labels[n[0]], sub=sub)
转换之后,整个库不到 10 MB,可以方便地放进任何实验仓库。要注意 physical=True 是默认值,它会把整数按增益与基线换算成毫伏;如果出于某种原因想保留整数,用 physical=False,但那会让不同记录之间的幅度失去可比性。
5.5 组间比较的统计量
拿到波形后,最自然的第一个实验是算几个常用指标,看三组之间有没有肉眼可见的差别:
import numpy as np
import wfdb
def rr_series(name, sub):
ann = wfdb.rdann(name, 'qrs', pn_dir=f'aftdb/1.0.0/{sub}')
return np.diff(np.asarray(ann.sample, dtype=float)) / 128.0 #-- 秒
def metrics(rr):
sdnn = float(np.std(rr, ddof=1))
rmssd = float(np.sqrt(np.mean(np.diff(rr) ** 2)))
pnn50 = float(np.mean(np.abs(np.diff(rr)) > 0.05))
return sdnn, rmssd, pnn50
for g in 'nst':
rows = [metrics(rr_series(f'{g}{i:02d}', 'learning-set')) for i in range(1, 11)]
arr = np.array(rows)
print(g.upper(), 'SDNN=%.4f RMSSD=%.4f pNN50=%.3f'
% tuple(arr.mean(axis=0)))
这段代码只打印三组的均值,不做任何显著性检验,也不下结论。它的用途是让你先建立对尺度的直觉:房颤的 RR 序列标准差通常远高于窦性心律,三组之间的差异如果只体现在小数点后第三位,那么单靠这几个指标是不足以做预测的,需要转向 f 波频域特征。这一步不要跳过,也不要直接引用这里打印出的数字当作结论——10 段样本的组均值波动很大,任何比较都应当在全部 80 段或多重划分下重复。
5.6 五个最容易出错的地方
| 位置 | 错误写法 | 后果 | 正确做法 |
|---|---|---|---|
| 标签读取 | 去头文件找 group 字段 | 头文件里没有 | 解析文件名首字母 |
| 数据集划分 | 按记录名随机切分 | s 与 t 成对泄漏 | 按 s/t 配对整体归组 |
| 通道理解 | 认为通道一就是 II 导联 | 官方未定义导联 | 写作"通道一/通道二" |
| 标注使用 | 用 .qrs 做类型分类 |
类型被抹平 | 只用位置,或自己检测 |
| 测试集评估 | 声称在官方测试集上有分数 | 官方未公布逐条真值 | 自建划分或引用文献成绩 |
§6 与库内同类记录的关系
6.1 关系图谱
房颤相关记录在库内已有一批,它们按"任务"而不是按"病"分布:
| 记录 | 记录规模 | 单段时长 | 任务 | 与 AF Termination 的关系 |
|---|---|---|---|---|
| MIT-BIH 房颤库(mit-bih-afdb) | 25 段 | 10 小时 | 房颤检测与节律标注 | 提供上下文,本库提供终止断面 |
| 长期房颤库(ltafdb) | 84 段 | 24–25 小时 | 长程负荷 | 同样来自长程记录,但目标是负荷而非终止 |
| PAF 预测挑战赛库(afpdb) | 100 段 | 30 分钟 | 房颤发作预测 | 方向相反:它预测"何时开始",本库预测"何时结束" |
| CinC 2017(physionet-cinc-2017) | 8528 段 | 9–61 秒 | 单导联房颤检测 | 规模差两个数量级,任务完全不同 |
| Kingston ICU AF(kingston-icu-af) | ICU 床旁短片 | 10 秒 | ICU 房颤检测 | 场景不同(ICU 床旁),本库是长程监护摘录 |
| INCART(incartdb) | 75 段 | 30 分钟 | 十二导联心律失常 | 导联更全,但无终止标签 |
这张表里最值得划一条线的是 afpdb 那一行。afpdb 预测的是"房颤什么时候开始",本库预测的是"房颤什么时候结束"。两者在时序上首尾相接,构成一对互补的命题:一个是发作的起点,一个是发作的终点。把两者混淆,是搜索时最容易发生的事故——因为它们的中文名里都有"房颤"与"预测"。
6.2 检索路径
| 你要找的东西 | 检索词 | 会落到哪里 |
|---|---|---|
| 房颤终止预测的原始数据 | AF termination、aftdb、spontaneous termination |
本篇 |
| 房颤发作预测 | PAF prediction、afpdb、paroxysmal |
afpdb 篇 |
| 长时程房颤负荷 | AFDB、LTAFDB、AF burden |
mit-bih-afdb / ltafdb 篇 |
| 大体量房颤检测 | CinC 2017、single-lead AF |
physionet-cinc-2017 篇 |
| 挑战赛数据集总览 | PhysioNet challenge、Computers in Cardiology |
本篇章 §8 与 cinc 系列 |
| 心电信号基础格式 | WFDB、annotations |
incartdb 篇的格式章节 |
6.3 与其他库的分工判据
| 任务 | 主粮仓 | AF Termination 的位置 | 原因 |
|---|---|---|---|
| 房颤检测预训练 | CinC 2017 或更大体量的库 | 不参与 | 无负类 |
| 房颤终止预测 | 无公开大库 | 唯一的公开基准 | 任务唯一 |
| 发作预测 | afpdb | 不参与 | 方向相反 |
| 长程负荷统计 | ltafdb / mit-bih-afdb | 不参与 | 只有一分钟 |
| 终止前电生理研究 | 无替代 | 主力 | 唯一带终止标签的公开波形 |
| 教学中的泄漏示例 | 任何多段同源库 | 最佳示例之一 | s/t 配对关系官方写得最清楚 |
最后一行可能听起来不像正经用途,但对工程团队来说它是实打实的价值:与其用一段虚构的例子讲数据泄漏,不如用这个库——它的配对关系是官方明文写出的,不需要任何推断,任何人一看就懂。
§7 质量评估与局限性
7.0 阅读本节前的一个前置校正
在给这个库打分之前,必须先纠正一个几乎所有人都会犯的误判:把它当作一个"数据质量不高"的库来评价。
它的 80 段、20 人、无临床变量、标注未审核,单看每一项都像是缺陷。但这些都不是妥协的结果,而是设计的结果。挑战赛的目的是让全世界用同一批小样本比方法,样本必须小到人人下得完,标签必须简单到不需要临床背景就能理解。如果它当初做了 8000 段、带完整临床变量、标注由三位专家交叉复核,那么它就不是一道考题,而是一座粮仓——但2004 年的条件也做不出这样的库。
这个校正会改变五维评分的读法(§7.1):它的"规模"分很低,这是事实;但"任务唯一性"接近满分,这也是事实。把两者混在一起,就会得出"不值得用"的结论,而这个结论是错的。
7.1 五维评分
按 DAIMS 的常用维度打分,每项 1–5 分,分数针对"作为房颤终止预测研究数据"这一用途:
| 维度 | 分数 | 理由 |
|---|---|---|
| 数据规模 | 1 | 80 段、20 名受试者的学习集,训练价值几乎为零 |
| 标注质量 | 2 | 只有 QRS 位置且未审核,组标签虽可靠但粒度极粗 |
| 任务适配 | 5 | 公开领域内唯一以自发终止为目标的带标签波形库 |
| 文档完整 | 3 | 三组定义、切分规则写得很清楚,但导联、人群、采集设备均未说明 |
| 可获取性 | 5 | ODC-BY、免注册、整库几兆、wfdb 可直读 |
五项合计 16 分(满分 25)。这个分数结构很有辨识度:两端(规模、适配)拉得很开,中间三项不高不低。它不适合作为"综合评分高"的库来推荐,但任何一个做终止预测的人,绕不开它。
7.2 标注质量的两层
第一层是组标签可不可信。这一层是整个库的地基,也是最可靠的一层。三组的定义是"终止发生在记录结束之后的某个时间范围内",这个判断由提供方在摘录时确定,不依赖任何算法。所以 N、S、T 的划分本身是准的。
第二层是逐事件标注可不可信。这一层几乎不可用。.qrs 由自动检测器生成、未经审核、异位搏动被标为正常。官方在三个地方(数据描述、ANNOTATORS 文件、数据下载说明)重复了"未审核"这一点,语气之重在其他库里不常见。
| 用途 | 组标签的影响 | QRS 标注的影响 | 建议 |
|---|---|---|---|
| 三分类终止预测 | 直接作为监督信号 | 只用于切 RR 序列 | 可用,注意配对泄漏 |
| RR 间期统计 | 无关 | 少量错误会引入噪声 | 可容忍,报告时说明 |
| 心搏类型统计 | 无关 | 完全不可用 | 自行检测 |
| 终止时刻定位 | 只能给到组级别 | 无关 | 本库不支持更细的定位 |
7.3 已知的、可枚举的偏差
| 偏差 | 表现 | 影响 |
|---|---|---|
| 样本极小 | 学习集 30 段、20 人 | 任何组间比较的置信区间都很宽 |
| 单中心 | 原始记录只注明来自西北大学一人提供 | 设备、贴电极方式、判读习惯都单一 |
| 人群未知 | 无年龄、性别、基础病、用药 | 无法做亚组分析,也无法评估代表性 |
| 终止方式混杂的澄清 | 官方只讲自发终止,未讲是否有人在监护中 | 不能推断有无抗凝、有无合并用药 |
| 配对泄漏 | s/t 是同一段连续记录的两半(§4.3 的案例) | 随机划分会虚高 |
| 窗口外终止 | 终止点不在记录内 | 无法学习终止瞬间的形态 |
| N 组定义宽松 | "一小时内未观察到终止"不等于不会终止 | 负类里混有假阴性 |
| 标注未审核 | QRS 自动检测 | 时间敏感指标带误差 |
| 导联未说明 | 两路都写 ECG | 无法做导联特异性结论 |
7.4 外部验证矩阵
这个库在文献里最常见的用法,不是训练,而是"拿来做特征有效性的检验场"。
| 验证角色 | 做法 | 讲得通的地方 | 风险 |
|---|---|---|---|
| 特征筛选 | 用学习集 30 段比较不同特征的区分度 | 三组时间断面清晰,特征差异容易显形 | 30 段上筛出的特征极易过拟合 |
| 方法对照 | 在同一划分下比较熵、频域、非线性方法 | 文献里已有一批这样的对照 | 划分口径不统一时结果不可比 |
| 外部小测 | 训练在主库,测在本库 | 换人群、换设备的检验 | 测试集无真值,只能自制划分 |
| 机理研究 | 提取 f 波的主导频率与组织化指数 | 波形原始、未做滤波,适合自己处理 | 只有两路,且导联未知 |
| 教学示例 | 演示泄漏与配对设计 | 官方明文写出配对关系 | 仅教学用途 |
这里要特别说明"外部小测"那一行的风险。由于官方没有公布测试集的逐条真值,任何声称"在官方测试集上取得某分数"的说法都需要核对来源:要么是当年参赛队伍的历史成绩(有成绩页可查),要么是作者自行按三组定义重建的划分(应写明重建方式)。把两者混为一谈,是复现研究里最常见的错误之一。
7.5 它为何能持续:一个小小的构造至今没人重做
二十多年过去,房颤数据集出了很多,规模从几十段涨到几万段,但"自发终止"这个标签几乎没有第二个公开库提供。原因是构造这类数据需要的东西恰好最难获得:不仅有房颤记录,还要知道每段记录之后发生了什么,且愿意把这个时间关系公开。
同时,这个库的构造方式又极其轻便——不需要额外采集,只需要在既有长程记录上做标注。理论上任何一家有长程房颤监护的医院都能复制一个更大的版本。事实上没人公开做,这本身就说明问题不在技术,而在"终止之后的随访信息"这件事的稀缺性。只要这个稀缺性还在,它作为唯一公开基准的地位就不会被规模更大的库取代。
§8 生态位与历史
8.1 历史沿革
这个库诞生在 PhysioNet 挑战赛的早期。2000 年代初,PhysioNet 每年办一次挑战赛,主题轮换,数据随赛事发布并永久留存。2004 年这一届的主题是房颤自发终止,是当年少数直接指向"预测"而非"识别"的题目。
它的历史价值有一部分来自这个时间点。2004 年深度学习尚未进入心电领域,参赛方法以特征工程加浅层分类器为主——熵、频谱、非线性动力学、RR 序列统计量。这批方法后来被大量引用,而它们全部是在这个库上比的。今天再读那些论文,这个库就是它们的共同实验台。
8.2 与挑战赛的关系
那一届挑战赛分两个事件,与本库的两个测试集一一对应:
| 事件 | 对应测试集 | 任务 | 官方给出的满分 | 冠军成绩 |
|---|---|---|---|---|
| 事件一 | test-set A(30 段) | 在 N 与 T 之间区分 | 30 分 | 29 分(97%),西北大学 Petrutiu 等 |
| 事件二 | test-set B(20 段) | 在 S 与 T 之间区分 | 20 分 | 20 分(100%),同一团队 |
这两行数字来自挑战赛成绩页。它们透露的信息比分数本身更有意思:事件二的满分被拿到了,事件一的最好的成绩差一分。这说明"一分钟后终止"与"一秒内终止"之间的区分,在当年的方法下是可以做到完美的;而"终止与不终止"的区分反而更难,因为 N 组里有大量"看起来快要停了却没停"的干扰样本。
这一点对今天的使用者仍然是提醒:这个库最难的地方不在识别终止前兆,而在排除假阳性。
8.3 生态位:考题而不是粮仓
| 角色 | 粮仓 | 考题 |
|---|---|---|
| 典型用法 | 大规模训练、预训练 | 小样本方法对照、外部检验 |
| 需要的前提 | 数据量大、标签细 | 任务唯一、划分清楚 |
| 代表库 | CinC 2017、大体量房颤库 | 本库、其他挑战赛小库 |
| 失败模式 | 过拟合、标签噪声累积 | 样本太小导致结论不稳 |
| 论文里的写法 | “我们在 X 上训练” | “我们在 Y 上验证了该方法” |
把考题当粮仓用,会立刻撞上 80 段的墙;把粮仓当考题用,会遇到"自己测自己"的问题。两者的边界不在数据格式上,而在样本量与标签粒度上。
8.4 谁在用它,用来做什么
从公开文献的摘要看,使用者大致分三类:做房颤终止预测的方法研究者(把它当作标准对照)、做心电特征工程的研究组(把它当作特征筛选的场地)、以及教学与科普场景(用它演示"预测"与"识别"的区别)。第三类最少见于正式文献,但它在实际传播中影响很大——很多人在学心电信号处理时,第一份带"预测"味道的数据就是这个库。
§9 使用指引
9.1 三种角色的路径
| 角色 | 第一天 | 第一周 | 注意事项 |
|---|---|---|---|
| 算法研究者 | 下载学习集,读头文件,跑通 §2.3 的读取 | 实现两三个文献特征,做受试者级划分的基线 | 先定划分再调参 |
| 临床研究者 | 看三组定义与时间断面 | 评估它能否回答你的临床问题 | 它没有用药与随访,临床问题多半答不了 |
| 教学人员 | 用 s01 与 t01 讲配对与泄漏 | 讲清"识别"与"预测"的区别 | 强调终止点不在记录内 |
9.2 一条最短的可用流程
#-- 1. 整包下载
curl -L -o aftdb.zip https://physionet.org/static/published-projects/aftdb/aftdb-1.0.0.zip
unzip -q aftdb.zip -d . && cd aftdb-1.0.0
#-- 2. 核对文件数与校验和
wc -l RECORDS
sha256sum -c SHA256SUMS.txt --quiet && echo "all files verified"
#-- 3. 列出学习集三组各多少条
grep -c '^learning-set/n' RECORDS
grep -c '^learning-set/s' RECORDS
grep -c '^learning-set/t' RECORDS
import wfdb
import numpy as np
#-- 4. 以 s/t 配对为分组键,做不泄漏的划分
pairs = {}
for i in range(1, 11):
pairs[f's{i:02d}/t{i:02d}'] = [f's{i:02d}', f't{i:02d}']
train_keys = list(pairs)[:7]
test_keys = list(pairs)[7:]
train = [r for k in train_keys for r in pairs[k]] + [f'n{i:02d}' for i in range(1, 8)]
test = [r for k in test_keys for r in pairs[k]] + [f'n{i:02d}' for i in range(8, 11)]
print(len(train), len(test)) #-- 21 9
上面这段划分演示了一个要点:s 与 t 必须成对进出。把 s01 放进训练、t01 放进测试,就是在允许模型"记住这场房颤"——这正是 §4.3 的案例要指出的那种泄漏。
9.3 引用方式
PhysioNet 对该库给出的原文引用是:
| 引用对象 | 内容 |
|---|---|
| 数据集本体 | Moody GB. Spontaneous Termination of Atrial Fibrillation: A Challenge from PhysioNet and Computers in Cardiology 2004. Computers in Cardiology 31:101–104 (2004). |
| 版本号 | 1.0.0,DOI 10.13026/C2CC7Z |
| 平台 | 按官方页面 Cite 按钮给出的平台综述引用 |
| 赛事 | PhysioNet/Computers in Cardiology Challenge 2004 的成绩页与公告 |
写论文时应同时注明版本与使用的集合(学习集还是测试集、哪一个事件),因为"用了本库"这句话本身有几个不同含义。
§10 AI 使用声明
| 项目 | 声明 |
|---|---|
| 文本生成 | 本文由 AI 辅助撰写,结构与事实由编辑把关 |
| 数字来源 | 规模、结构、边界三类数字取自 PhysioNet 项目页、挑战赛页与官方文件 |
| 文件核验 | RECORDS、ANNOTATORS、多条头文件(n01、s01、t01、a01、a30、b01、b20)经直接访问确认 |
| 未核验内容 | 全库每条记录的增益全貌、各组的 RR 统计量、测试集逐条真值 |
| 论文信息 | 未读到 2004 年原文全文,赛事信息取自官方成绩页与公告 |
| 导联位置 | 官方与头文件均未定义两路信号取自哪个导联,本文不使用 II/V1 的对应 |
| 图像 | 无配图;封面为生成式提示词,不含真实心电图 |
| 免责 | 不构成临床建议;使用须遵守 ODC-BY 1.0 署名要求 |
常见误读(避坑清单)
| 说法 | 实际 | 证据 |
|---|---|---|
| 「它是电复律房颤数据集」 | 官方全文用 spontaneous,指自发终止 | 项目页数据描述与挑战赛公告 |
| 「它是药物终止房颤的研究数据」 | 无用药、无干预时刻 | 头文件与 README 均无相关字段 |
| 「它有 100 段记录」 | 80 段(30 + 30 + 20) | RECORDS 共 80 行 |
| 「它能用来训练房颤检测模型」 | 全部记录都是房颤,没有负类 | 三组定义都预设"这段是房颤" |
| 「它有房颤终止时刻的标注」 | 终止点被放在记录结束之后 | 官方 S 组与 T 组的定义 |
| 「它的 QRS 标注可以拿来做类型分类」 | 所有心搏一律标为正常 | 项目页与 ANNOTATORS |
| 「测试集有公开真值可以刷分」 | 官方未公布逐条真值 | 项目页无 ground truth 文件 |
| 「按记录随机划分就行」 | s 与 t 是同一次记录的两半 | 时间戳差 61 秒、增益完全相同 |
| 「两个通道就是 II 与 V1」 | 头文件两路都只写 ECG,未见任何电极位置说明 | *.hea 第 2、3 行末字段与注释行 |
| 「它和 CinC 2017 是同一届挑战赛」 | 相差十三年,任务与规模完全不同 | 两处项目页 |
FAQ
Q1:这个库里的房颤是被电复律或药物终止的吗?
不是。官方用词是 spontaneous termination(自发终止),指没有干预的情况下房颤自己停下。库内没有电复律记录,也没有抗心律失常药物的给药时刻。把它当成复律疗效数据用,会从根上搞错标签含义。
Q2:一共多少段、多少名受试者?
80 段:学习集 30 段(n、s、t 各十段)、测试集 A 30 段、测试集 B 20 段。受试者方面,学习集 20 名(s 与 t 共用十人)、测试集 A 30 名、测试集 B 10 名,三个集合互不重叠。
Q3:有没有标注房颤终止的具体时刻?
没有,而且是刻意不标。s 组在记录结束后约一分钟终止,t 组在结束后一秒以内终止。终止那一下本身不在任何一段记录里,能被利用的只有终止之前最长一分钟的心电形态。
Q4:能不能用它训练心搏分类?
不建议。.qrs 由自动检测器生成、未经审核,并且把所有检出的心搏(含异位搏动)一律标为正常。文件里不存在类型信息,只能当位置提示用。
Q5:测试集真值在哪里?
官方项目页没有公布逐条真值。挑战赛成绩页给出了两个事件的满分(30 与 20)以及参赛队伍的成绩。想评测自己的方法,要么自行按三组定义重建划分,要么与文献成绩做对照,不能声称在官方测试集上取得了可比分数。
Q6:为什么 s01 和 t01 看起来像同一段记录?
因为它们确实是。官方明确说 t 组记录紧跟 s 组记录,t01 是 s01 的接续。头文件的时间戳相差 61 秒,两路增益完全相同,这两个证据都支持这一点。
Q7:每段为什么正好一分钟?
7680 个采样点除以 128 Hz 等于 60 秒。这个长度是从长程记录里截取时对齐到的整分钟,好处是切窗时没有零头。
Q8:两个通道是哪两个导联?
官方没有说明,头文件里两路信号的描述字段都写作 ECG,也没有电极位置的注释行。这意味着任何"这个通道是 II 导联"的说法都缺少官方依据,做导联特异性结论无从谈起。
Q9:许可允许商业使用吗?
ODC-BY 1.0 允许,包括商业用途,条件是署名。引用时应写清数据集名、版本 1.0.0 与 DOI。
Q10:数据集有多大?下载方便吗?
官方页面写占用约 3.3 MB、解压后 2.4 MB。三条下载途径:整包 ZIP、wget 递归、AWS 命令行同步。任何一条在普通网络下都是秒级完成。
Q11:它和 afpdb(PAF 预测挑战赛库)有什么区别?
afpdb 预测"房颤什么时候开始",本库预测"房颤什么时候结束"。方向相反,样本规模与时长也不同:afpdb 是 100 段三十分钟记录,本库是 80 段一分钟记录。搜索时两者容易混淆。
Q12:为什么二十多年了没有扩版?
官方没有任何扩版说明。从构造方式看,扩版需要更多"知道终止之后发生了什么"的长程记录,而这个信息比房颤记录本身更难获得,这更像是现实约束而不是计划中断。
术语表
| 术语 | 解释 |
|---|---|
| 自发终止 | 无外部干预的情况下房颤自行转复,对应英文 spontaneous termination |
| 时间断面 | 本库的核心构造:记录与终止事件之间的时间关系 |
| N 组 | 在其后至少一小时内未观察到终止的房颤段 |
| S 组 | 在记录结束后约一分钟终止的房颤段 |
| T 组 | 在记录结束后一秒以内终止的房颤段 |
| 配对泄漏 | s 与 t 本是同一段记录的两半,分置训练与测试两侧造成的信息泄漏 |
| f 波 | 房颤时心房的不规则快速电活动在体表心电上的投影 |
| 主导频率 | f 波能量最集中的频率,常被用作终止预测特征 |
| 组织化指数 | 衡量 f 波规则程度的指标,与终止倾向相关 |
| RR 间期 | 相邻两次心室激动的间隔,房颤时高度不规则 |
| SDNN | RR 间期的标准差,最常用的心率变异指标之一 |
| RMSSD | 相邻 RR 间期差值的均方根 |
| pNN50 | 相邻 RR 间期差值超过 50 毫秒的比例 |
| WFDB | PhysioNet 使用的波形数据库格式,含头文件、信号、注释 |
.hea |
纯文本头文件,声明采样率、增益与起始时刻 |
.dat |
二进制信号文件,两路交错存放 |
.qrs |
二进制注释文件,本库存放未审核的 QRS 位置 |
| 增益 | 每毫伏对应的整数单位数,换算物理量时使用 |
| 校验和 | 采样点累加和,用于检测文件完整性 |
| 挑战赛事件 | 同一届挑战赛下相互独立的评分任务 |
| 自动计分器 | 挑战赛使用的在线评分工具,提交后即时返回分数 |
| ODC-BY | 开放数据共享署名许可 |
| PhysioNet | 由 MIT 等机构维护的生物医学信号数据平台 |
| Holter 式记录 | 便携式长时程心电监护,通常一到两个通道 |
附录 A:核心数字核对表
| 项 | 值 | 来源 | 来源层级 |
|---|---|---|---|
| 记录总数 | 80 | RECORDS 逐行计数 | 官方文件 |
| 学习集记录数 | 30 | 官方页面;RECORDS | 官方页面 |
| 测试集 A 记录数 | 30 | 官方页面;RECORDS | 官方页面 |
| 测试集 B 记录数 | 20 | 官方页面;RECORDS | 官方页面 |
| 学习集受试者数 | 20 | 官方页面 | 官方页面 |
| 测试集 A 受试者数 | 30 | 官方页面 | 官方页面 |
| 测试集 B 受试者数 | 10 | 官方页面 | 官方页面 |
| 每段采样点 | 7680 | 多条头文件第 1 行 | 官方文件 |
| 采样率 | 128 Hz | 官方页面;头文件 | 官方页面 |
| 每段时长 | 60 秒 | 7680 ÷ 128 | 官方文件推算 |
| 通道数 | 2 | 官方页面;头文件 | 官方页面 |
| 增益量级 | 约 140–202 /毫伏 | 抽查头文件 | 官方文件 |
| s01 起始时刻 | 19:32:13 | s01.hea | 官方文件 |
| t01 起始时刻 | 19:33:14 | t01.hea | 官方文件 |
| 整库体积 | 约 3.3 MB | 官方页面 | 官方页面 |
| 事件一满分 | 30 | 挑战赛成绩页 | 官方页面 |
| 事件二满分 | 20 | 挑战赛成绩页 | 官方页面 |
| 许可 | ODC-BY 1.0 | 官方页面 | 官方页面 |
| 首版日期 | 2004-04-11 | 官方页面 | 官方页面 |
| 版本 | 1.0.0 | 官方页面 | 官方页面 |
附录 B:数字的来源分层
第一层:官方页面与官方文件。 记录数、受试者数、三组定义、采样率、时长、体积、许可、日期、赛事满分与名次,以及头文件里的增益与时间戳。项目页的数据描述段与挑战赛成绩页各自支撑一部分。
第二层:论文摘要与文献。 关于这类数据在文献中的常见用法(特征筛选、方法对照、外部小测),来自检索到的综述与摘要;关于两路信号与任何具体导联的对应,官方未定义,本文不采用文献中的类推。
第三层:未核实。 全库每条记录的增益全貌、各组的 RR 统计量与主导频率分布、测试集逐条真值、N 组样本中实际后来终止的比例。这些都需要逐条计算或需要未公开的信息,正文涉及处均只做定性表述。
附录 C:完整参考文献
| 编号 | 文献 | 链接 | 用途 |
|---|---|---|---|
| 1 | AF Termination Challenge Database v1.0.0 项目页 | https://physionet.org/content/aftdb/1.0.0/ | 全部规模、结构、边界数字的主来源 |
| 2 | 该库文件目录 | https://physionet.org/files/aftdb/1.0.0/ | RECORDS、ANNOTATORS、各记录头文件 |
| 3 | 挑战赛页面(含成绩与公告) | https://physionet.org/content/challenge-2004/ | 事件划分、满分、冠军成绩、时间线 |
| 4 | Moody GB, CinC 31:101–104 (2004) | http://www.cinc.org/archives/2004/pdf/101.pdf | 官方指定引用(本文未读全文) |
| 5 | PhysioNet 平台综述(2026) | https://doi.org/10.1038/s44360-026-00096-z | 托管模式与引用规范 |
| 6 | PhysioBank 注释定义 | https://physionet.org/physiobank/annotations.shtml | QRS 注释符号的标准含义 |
| 7 | MIT-BIH 房颤库(对照) | https://physionet.org/content/afdb/ | §2.5 与 §6.1 对比 |
| 8 | PAF 预测挑战赛库(对照) | https://physionet.org/content/afpdb/ | 方向相反的任务线 |
附录 D:任务代码
任务一:给每条记录打上组标签并做泄漏安全的划分
import re
import wfdb
def group_of(name):
#-- 组标签只存在于文件名首字母,头文件里没有
m = re.match(r'^([nstab])', name)
return {'n': 'N', 's': 'S', 't': 'T',
'a': 'A', 'b': 'B'}[m.group(1)]
def split_safe(seed=0):
import random
rng = random.Random(seed)
#-- s 与 t 必须成对进出,否则同一场房颤会被切开
units = [[f's{i:02d}', f't{i:02d}'] for i in range(1, 11)]
units += [[f'n{i:02d}'] for i in range(1, 11)]
rng.shuffle(units)
cut = int(len(units) * 0.7)
train = [r for u in units[:cut] for r in u]
test = [r for u in units[cut:] for r in u]
return train, test
tr, te = split_safe()
print(len(tr), len(te), sorted(set(map(group_of, tr + te))))
split_safe 把 s 与 t 绑定成一个不可拆分的单元,n 段各自独立,这样划分出来的训练集与测试集不会共享同一场房颤。用不同的 seed 重复几次,可以估计成绩的波动幅度。
任务二:一分钟窗口内的 f 波主导频率估计
import numpy as np
import wfdb
def dominant_freq(name, sub, fs=128.0, lo=3.0, hi=12.0):
rec = wfdb.rdrecord(name, pn_dir=f'aftdb/1.0.0/{sub}', physical=True)
x = rec.p_signal[:, 0] #-- 只用通道一
x = x - np.mean(x)
spec = np.abs(np.fft.rfft(x * np.hanning(len(x))))
freqs = np.fft.rfftfreq(len(x), 1.0 / fs)
band = (freqs >= lo) & (freqs <= hi)
return float(freqs[band][np.argmax(spec[band])])
for g in 'st':
vals = [dominant_freq(f'{g}{i:02d}', 'learning-set') for i in range(1, 11)]
print(g.upper(), 'median peak =', round(float(np.median(vals)), 3), 'Hz')
第二段代码是最粗糙的 f 波频率估计:直接对整段做 FFT 取 3–12 Hz 内的峰值。真实使用中必须先去 QRS(常用办法是做模板减除或用带通滤波压低 QRS 能量),否则峰值会被 QRS 的谐波带偏。这里保留粗糙版本,是为了让读者看到"先跑通、再改进"的顺序——先确认频带与采样率都没问题,再考虑用更复杂的去 QRS 方法。注意 128 Hz 的采样率下,奈奎斯特频率只有 64 Hz,对 f 波分析够用,对更高频的 QRS 细节就不够了。
附录 E:如果只记五件事
| 序号 | 记住什么 | 为什么 |
|---|---|---|
| 1 | 80 段、一分钟、双通道、128 Hz、三组标签 | 这五个数字定义了一切用法与限制 |
| 2 | 是自发终止,不是电复律或药物 | 搞错这一点,全部结论都偏 |
| 3 | 终止点被放在记录之外 | 题目问的是"窗外会发生什么" |
| 4 | s 与 t 是同一场房颤的两半 | 任何划分都必须成对处理 |
| 5 | 只有 QRS 位置标注,且未审核 | 心搏类型与节律类型都不在库里 |
编辑说明
核验时间为 2026 年 10 月 28 日。核对方式为直接访问 PhysioNet 的 AF Termination Challenge Database 项目页、挑战赛页面与文件目录,逐项确认 RECORDS 的 80 行、ANNOTATORS 的一行内容,以及 n01、s01、t01、a01、a30、b01、b20 七条记录头文件的采样率、采样点数、增益与时间戳。2004 年原始论文全文未能取得,赛事相关信息以官方成绩页与公告为准。未覆盖的内容包括:全库每条记录的增益分布、各组的 RR 与 f 波统计量、测试集逐条真值、N 组样本的后续转归。这些要么需要逐条计算,要么根本未公开,本篇按要求只写规模、结构、边界三类数字。
相关数据集导航
以下为站内 AI-Ready 数据集百科中与本词条共享多个主题标签的相关数据集,按相关度降序排列:
- afpdb — 共享标签:生理信号 / 心电信号 / 心血管疾病 / 挑战赛数据集
- incartdb — 共享标签:生理信号 / 心电信号 / 心血管疾病 / 挑战赛数据集
- twadb — 共享标签:生理信号 / 心电信号 / 心血管疾病 / 挑战赛数据集
- cinc-2021 — 共享标签:生理信号 / 心电信号 / 心血管疾病 / 挑战赛数据集
- ltafdb — 共享标签:生理信号 / 心电信号 / 心血管疾病 / 挑战赛数据集
- cinc-2011 — 共享标签:生理信号 / 心电信号 / 心血管疾病 / 挑战赛数据集
- cinc-2020 — 共享标签:生理信号 / 心电信号 / 心血管疾病 / 挑战赛数据集
- butppg — 共享标签:生理信号 / 心电信号 / 心血管疾病 / 挑战赛数据集
- ecg-image-database — 共享标签:生理信号 / 心电信号 / 心血管疾病 / 挑战赛数据集
- georgia-12-lead-ecg-challenge — 共享标签:生理信号 / 心电信号 / 心血管疾病 / 挑战赛数据集
导航说明:本章节由全站统一标签体系自动计算生成(标签重合度算法),双向可达;点击链接可跳转至对应数据集词条。

