简介:12导联心电图(ECG)数据集共包含39732条心电记录,已按7:3划分为训练集与测试集,适合医疗AI、心电信号处理研究者及数据科学学习者用于异常检测、心率变异分析及深度学习模型训练。压缩包内共45个文件,以37个csv数据文件为主,辅以XML配置及说明文档,整体大小10.75MB,目录结构清晰,并配有标签标注信息,便于直接开展监督学习。已有4081人学习下载,是验证分类算法与神经网络结构的实用数据。基于该数据可开展心律不齐识别、心肌梗死检测等任务,也可结合CNN、RNN等模型进行端到端特征提取与心电信号分类实验,可用于科研与教学实践。
1. 拿到39732条12导联心电图数据:这份ECG数据到底能做什么
你搜索"ecg 心电图数据",大概率是手头有个分类任务或者异常检测任务要做,却苦于找不到一份能直接喂给模型的标注数据。这份12导联心电图数据集一共39732条样本,以CSV格式按UUID命名,每条记录对应一个心动周期的12导联信号,配套的train_label.csv给出了标签。7:3的训练测试划分意味着你拿到手不需要自己再切分,直接可以开始建模。适合做心律失常分类、心肌梗死识别、心率变异性分析,也可以作为CNN、LSTM等深度学习模型的心电信号输入。我不打算给你讲心电图学的教科书内容,而是从踩过的坑出发——CSV列到底怎么排、标签文件怎么对齐、信号要不要滤波、切片怎么做不会把心拍切碎。这些都搞定了,剩下的就是模型选型和调参的事。
2. 先摸清数据底细:CSV结构、标签对齐与信号可视化
2.1 文件清单与导联映射:UUID命名的CSV里藏着什么
解压后你会看到大量类似0a0a3836-35ce-4c0d-9b8f-453676034d7e.csv的文件,外加一个train_label.csv。文件名的UUID就是样本ID,和标签文件里的ID一一对应。每行CSV是一个采样点,列对应各导联的电压幅值。这里有个常见做法是用pandas直接读单个CSV先看结构。
import pandas as pd sample_path = "0a0a3836-35ce-4c0d-9b8f-453676034d7e.csv" df = pd.read_csv(sample_path, header=None) print(df.shape) print(df.head())逻辑说明:读取时不指定列名,是因为文件名本身不含导联信息,列顺序需要你自己确认。常见的数据组织方式有两种——按行存储一个心拍的12导联采样序列,或按列存储每个导联的独立时间序列。
参数说明:header=None防止把第一行数据当列名,df.shape输出形如(N, 12)的行列数,N是采样点数,12就是标准12导联。如果shape是(12, N),说明数据转置存储了,后面处理要统一成(N, 12)。
再读标签文件:
labels = pd.read_csv("train_label.csv") print(labels.head()) print(labels["label"].value_counts())逻辑说明:确认标签列名和类别分布。这一步决定了你的任务是二分类还是多分类。如果是多分类,后续模型输出层和损失函数都要跟着调整。
参数说明:value_counts()输出每个类别的样本数,如果某个类别占比低于5%,训练时就要考虑类别权重或者过采样。
2.2 数据加载与标签对齐:把train_label.csv和样本CSV对上
所有样本都要和标签配好,才能构造训练数据集。常见做法是先列目录拿到所有CSV文件名,再把每个文件读进来,按文件名映射标签。
import os import numpy as np data_dir = "ecg数据" label_df = pd.read_csv("train_label.csv") label_map = dict(zip(label_df["id"], label_df["label"])) all_files = [f for f in os.listdir(data_dir) if f.endswith(".csv")] X_list, y_list = [], [] for fname in all_files: sample_id = fname.replace(".csv", "") if sample_id not in label_map: continue sig = pd.read_csv(os.path.join(data_dir, fname), header=None).values X_list.append(sig) y_list.append(label_map[sample_id]) X = np.stack(X_list, axis=0) y = np.array(y_list) print("X shape:", X.shape, "y shape:", y.shape)逻辑说明:np.stack把每条样本堆叠成三维张量,shape是(样本数, 采样点数, 导联数)。注意先统一浮点类型,后面标准化和模型输入都基于这个张量。
参数说明:label_map用dict存储映射关系,查询复杂度为O(1)。跳过缺失标签的文件,防止读到没标注的数据把训练集搞脏。
2.3 信号可视化与基础统计:先看波形再谈建模
建模前必须画几条波形确认信号质量。直接上matplotlib画前几个样本的I导联和V1导联。
import matplotlib.pyplot as plt idx = 0 lead_idx = 0 # I导联 plt.figure(figsize=(12, 4)) plt.plot(X[idx][:, lead_idx]) plt.title(f"Sample {idx} - Lead I, label = {y[idx]}") plt.xlabel("sample point") plt.ylabel("amplitude") plt.grid(True) plt.show()逻辑说明:这一步回答三个问题——信号有没有明显基线漂移、幅值范围是否统一、波形形态是否符合心电图基本特征。如果波形看起来像一堆噪声,得先检查是不是导联顺序选错了。
参数说明:lead_idx对应导联顺序。如果导联顺序不是标准I、II、III、aVR、aVL、aVF、V1到V6,从这里就能看出来。幅值单位一般是毫伏(mV),后续标准化时不需要管单位,但做临床解释时要注意。
3. 信号预处理流水线:滤波、标准化与切片策略
3.1 去除基线漂移与高频噪声:带通滤波器参数怎么设
心电信号频率集中在0.5Hz到100Hz之间,常见做法是设计一个0.5到100Hz的带通滤波器,滤掉基线漂移和高频肌电干扰。我用scipy的Butterworth滤波器来实现。
from scipy.signal import butter, filtfilt def bandpass_filter(signal, lowcut=0.5, highcut=100.0, fs=500.0, order=4): nyquist = 0.5 * fs low = lowcut / nyquist high = highcut / nyquist b, a = butter(order, [low, high], btype="band") return filtfilt(b, a, signal, axis=0) X_filtered = np.zeros_like(X) for i in range(X.shape[0]): X_filtered[i] = bandpass_filter(X[i], fs=500.0) print("filtered shape:", X_filtered.shape)逻辑说明:filtfilt做零相位滤波,不会引入相位偏移,保证波形形态不被扭曲。axis=0表示沿时间轴滤波,12个导联一次性处理。
参数说明:fs=500.0假设采样率500Hz——需要从数据本身确认。如果实际采样率是250Hz或1000Hz,滤波器的截止频率换算到数字域时用的采样率必须改,否则滤波效果完全不正确。
3.2 标准化:全局标准化还是逐导联标准化
标准化策略直接影响模型收敛。我的习惯是先测试数据,看不同导联的幅值范围差异。
lead_std = np.std(X_filtered, axis=(0, 1)) print("per-lead std:", lead_std) X_mean = np.mean(X_filtered, axis=(0, 1), keepdims=True) X_std = np.std(X_filtered, axis=(0, 1), keepdims=True) X_norm = (X_filtered - X_mean) / (X_std + 1e-8) print("after norm, global mean:", np.mean(X_norm), "std:", np.std(X_norm))逻辑说明:如果各导联方差差异大,逐导联标准化更好,相当于每个导联独立做缩放。如果差异不大,全局标准化更省事。1e-8防除零。
参数说明:axis=(0, 1)表示在样本维和时间维上计算统计量,得到每个导联一个均值和一个标准差。
3.3 切窗策略:固定长度切片还是按心拍切
常见做法是固定长度切片——比如取采样点256或512个点作为一个窗口。这里有个取舍:窗口太小,心拍信息不完整;窗口太大,计算开销翻倍,模型训练时间拉长。
window_size = 256 stride = 128 X_windows, y_windows = [], [] for i in range(X_norm.shape[0]): sig = X_norm[i] n_samples = sig.shape[0] for start in range(0, n_samples - window_size + 1, stride): win = sig[start:start + window_size] X_windows.append(win) y_windows.append(y[i]) X_final = np.stack(X_windows, axis=0) y_final = np.array(y_windows) print("X_final:", X_final.shape, "y_final:", y_final.shape)逻辑说明:固定窗口加滑动步长,让一条样本扩展出多个窗口,增加训练数据量。stride=128意味着窗口间重叠50%,增加样本数量同时不引入过多冗余。
参数说明:窗口大小和stride的选择是经验值。如果样本原始长度不足256,上面这个循环会直接跳过——这意味着部分短样本被丢弃,最好单独检查样本长度分布再定窗口。
4. 避坑手册:ECG数据预处理和建模的五个典型翻车现场
4.1 导联顺序搞错导致模型学不到东西
现象:模型在训练集上准确率一直徘徊在50%左右,比随机猜好不了多少。我一度以为是模型结构问题。
原因:有些CSV的列顺序不是标准I、II、III、aVR、aVL、aVF、V1-V6,而是按采集设备通道顺序排列的。模型把所有导联当成固定顺序输入,顺序错了,空间位置的语义就完全变了。
解决:画图对比每个CSV前几列波形和标准心电图形态。最省事的办法是在加载阶段强制指定导联名称列表,再把列重排成标准顺序。代码参考前面2.3节的可视化方法,逐导联检查一遍。
4.2 采样率不一致导致信号解读错误
现象:同一个数据集里,部分样本看起来被拉得很长或压得很扁,特征提取后效果很差。
原因:样本可能来自不同采集设备,采样率不统一——有250Hz的、有500Hz的。滤波器的截止频率是按采样率换算的,采样率错了,滤波频率也全错了。
解决:加载时读取文件头或者CSV长度推断采样率,全部重采样到统一采样率。用scipy.signal.resample做重采样,注意重采样前要先滤波防止混叠。
4.3 标签文件有缺失,直接报错中断
现象:遍历样本文件时,遇到标签字典里查不到的文件名,程序直接KeyError中断。
原因:train_label.csv只覆盖了一部分样本,或者解压过程中部分文件损坏、遗漏。
解决:遍历时先判断if sample_id not in label_map,缺失标签的样本可以跳过。但要在日志里输出跳过数量,如果缺失比例超过10%,说明数据问题比较严重,建议回头检查压缩包完整性。
4.4 切窗后类别分布严重偏移
现象:切窗后训练集里某一类样本暴增,模型为了降低loss把所有样本都预测为这一类。
原因:每个原始样本切成多个窗口,如果某一类原始样本特别长或者特别多,窗口数就会不成比例地膨胀。
解决:切窗后重新统计y_final的分布,如果某一类占比超过70%,考虑在训练时用class_weight,或者对样本多的类别减少切窗数量,样本少的类别用更小的stride。
4.5 测试集验证时用了训练集的统计量
现象:训练集准确率98%,验证集准确率直接掉到60%。
原因:标准化时把训练集和测试集混在一起计算均值和标准差,导致信息泄漏——测试集的统计信息已经参与训练。
解决:先按索引切分训练集和测试集,只用训练集计算均值和标准差,测试集复用这套参数。这个坑我第一次处理时也踩了,从那以后每次划分数据集我都强制走一遍先切分再标准化的流程。
5. 快速验证技巧:从一个小模型起步,别一上来就堆深度网络
拿你自己的数据先检验预处理流程是否通顺,再谈模型效果。我一般会先用逻辑回归或者随机森林跑一遍,看基线水平如何。深度模型能不能学到东西,很大程度上取决于数据本身的信噪比——预处理做对了,传统机器学习模型就已经能看出分类的可能性。
用scikit-learn验证全流程:
from sklearn.ensemble import RandomForestClassifier from sklearn.model_selection import train_test_split from sklearn.metrics import classification_report n_samples = X_final.shape[0] X_flat = X_final.reshape(n_samples, -1) X_train, X_test, y_train, y_test = train_test_split( X_flat, y_final, test_size=0.3, random_state=42, stratify=y_final ) clf = RandomForestClassifier(n_estimators=100, max_depth=10, n_jobs=-1) clf.fit(X_train, y_train) y_pred = clf.predict(X_test) print(classification_report(y_test, y_pred))逻辑说明:先把三维信号展平成二维,每行是一个样本的全部导联数据。随机森林不依赖特征缩放,所以这里不需要额外标准化。stratify=y_final保证训练测试集类别分布一致,避免小类样本全跑到测试集里。
参数说明:test_size=0.3对应数据本身的7:3划分。n_jobs=-1用满所有CPU核,随机森林训练很快,是验证数据管线的首选方法。
如果随机森林的F1分数已经能到0.8以上,说明数据质量不错,可以继续上CNN或LSTM。如果连随机森林都只有0.6左右,不要急着换复杂模型,先回头检查滤波参数、窗口大小和标签正确性——大概率问题出在数据管线上,而不是模型容量。
真实模型训练时,记得把预处理的参数都用训练集的统计量计算。比如滤波参数不变,但标准化用的均值和标准差必须单独存下来。
import joblib joblib.dump(X_mean, "preprocess_mean.pkl") joblib.dump(X_std, "preprocess_std.pkl")逻辑说明:保存预处理参数,推理阶段加载同一套参数对线上样本做变换,保证模型输入分布和训练一致。
参数说明:X_mean和X_std的形状是(1, 1, 12),推理时广播到和目标信号相同的形状,直接做(x - X_mean) / (X_std + 1e-8)即可。
从那以后我每次处理ECG数据集,都会强制走一遍完整流程——先画波形确认导联顺序,再滤波,再切窗,最后用随机森林跑基线。这套流程走通了,后面无论是CNN还是Transformer架构,都只是换模型文件的事。数据管线稳了,模型的上下限才能如实反映出来。希望这篇笔记帮你说清楚这份12导联ECG数据集怎么用、坑在哪,也省掉自己摸索的时间。
本文还有配套的精品资源,点击获取