当我在波士顿儿童医院的科研合作项目中第一次接触CHB-MIT数据集时,那些看似简单的EDF文件里藏着令人着迷的神经密码。作为目前癫痫研究领域最权威的公开脑电数据库之一,它包含了22名患者长达数天的多通道头皮EEG记录,采样率256Hz,数据总量超过600小时。但原始数据就像未切割的钻石——需要专业的工具和流程才能释放其价值。本文将分享一套经过临床验证的Python处理流水线,从数据下载到特征提取,手把手带你穿越EEG分析的迷雾森林。
在开始解剖EDF文件之前,我们需要搭建一个稳定的Python工作环境。推荐使用conda创建独立环境以避免依赖冲突:
bash复制conda create -n eeg_analysis python=3.8
conda activate eeg_analysis
pip install mne pyedflib numpy pandas matplotlib scipy
CHB-MIT数据集托管在PhysioNet平台,获取需要三个步骤:
wget批量下载(约23GB):python复制import os
base_url = "https://physionet.org/files/chbmit/1.0.0/"
files = ["chb01.tar.gz", "chb02.tar.gz", ..., "chb24.tar.gz"]
for file in files:
os.system(f"wget {base_url}{file}")
os.system(f"tar -xzf {file}")
注意:国内用户可能需配置代理加速下载,解压后会得到以患者ID命名的文件夹(如chb01),每个包含:
EDF(European Data Format)是医疗设备常用的标准格式,其结构包含:
使用MNE-Python读取比直接解析更高效:
python复制import mne
def load_edf(edf_path):
raw = mne.io.read_raw_edf(edf_path, preload=True)
print(f"采样率: {raw.info['sfreq']}Hz")
print(f"通道: {raw.ch_names}")
return raw
# 示例:加载chb01第一个文件
raw = load_edf("chb01/chb01_01.edf")
典型输出显示23个EEG通道(FP1-F7, T7-P7等)和1个ECG通道。通过raw.plot()可交互查看信号,但全时段显示效果差,推荐提取片段:
python复制import matplotlib.pyplot as plt
plt.figure(figsize=(12,6))
raw.copy().crop(tmin=300, tmax=305).plot()
plt.title("5秒EEG片段(通道FP1-F7)")
plt.show()

图:典型癫痫发作间期的脑电波形
CHB-MIT的标注信息分散在三个位置:
构建标注解析器:
python复制def parse_seizure_annotations(patient_dir):
seizure_files = []
with open(f"{patient_dir}/RECORDS-WITH-SEIZURES", 'r') as f:
seizure_files = [line.strip() for line in f]
events = []
for file in seizure_files:
base_name = file.split('.')[0]
with open(f"{patient_dir}/{base_name}.seizure", 'r') as f:
lines = f.readlines()
for line in lines[1:]: # 跳过首行说明
start, end = map(float, line.split()[:2])
events.append((file, start, end))
return events
将标注转换为MNE事件:
python复制def create_mne_events(raw, seizure_events):
events = []
for _, start, end in seizure_events:
start_sample = int(start * raw.info['sfreq'])
end_sample = int(end * raw.info['sfreq'])
events.append([start_sample, 0, 1]) # 1表示发作开始
events.append([end_sample, 0, 2]) # 2表示发作结束
return np.array(events)
seizure_events = parse_seizure_annotations("chb01")
mne_events = create_mne_events(raw, seizure_events)
原始EEG信号需要五步清洗:
工频滤波:去除50/60Hz电源干扰
python复制raw.notch_filter([50, 60], fir_design='firwin')
带通滤波:保留0.5-70Hz有效频段
python复制raw.filter(0.5, 70, fir_design='firwin')
坏道修复:通过相邻通道插值
python复制raw.info['bads'] = ['T7-P7'] # 标记坏道
raw.interpolate_bads()
重参考:转换为平均参考
python复制raw.set_eeg_reference(ref_channels='average')
降采样:减少计算量
python复制raw.resample(128) # 降至128Hz
关键参数对比表:
| 步骤 | 推荐参数 | 作用 | 耗时(分钟/小时数据) |
|------|----------|------|---------------------|
| 工频滤波 | 50/60Hz | 去除电源干扰 | 2.1 |
| 带通滤波 | 0.5-70Hz | 保留生理信号 | 3.4 |
| 降采样 | 128Hz | 平衡信息与计算量 | 1.7 |
最终目标是生成机器学习可用的特征矩阵,常用时频域特征包括:
时域特征(每5秒片段):
python复制def extract_time_features(signal):
return {
'mean': np.mean(signal),
'std': np.std(signal),
'kurtosis': scipy.stats.kurtosis(signal),
'zero_crossings': ((signal[:-1] * signal[1:]) < 0).sum()
}
频域特征(FFT计算):
python复制def extract_freq_features(signal, sfreq):
psd = np.abs(np.fft.rfft(signal))**2
freqs = np.fft.rfftfreq(len(signal), 1/sfreq)
bands = {'delta': (0.5,4), 'theta': (4,8),
'alpha': (8,12), 'beta': (12,30),
'gamma': (30,70)}
features = {}
for band, (low, high) in bands.items():
mask = (freqs >= low) & (freqs <= high)
features[f'{band}_power'] = psd[mask].mean()
return features
构建完整数据集的技巧:
python复制def create_dataset(raw, events, window=5, stride=2.5):
X, y = [], []
sfreq = raw.info['sfreq']
n_samples = int(window * sfreq)
# 提取发作时段
seizure_mask = np.zeros(len(raw))
for onset, _, event_id in events:
if event_id == 1: # 发作开始
start = onset
elif event_id == 2: # 发作结束
seizure_mask[start:onset] = 1
# 滑动窗口采样
for i in range(0, len(raw)-n_samples, int(stride*sfreq)):
segment = raw[:, i:i+n_samples][0]
features = {**extract_time_features(segment),
**extract_freq_features(segment, sfreq)}
X.append(features)
y.append(1 if seizure_mask[i:i+n_samples].mean() > 0.5 else 0)
return pd.DataFrame(X), np.array(y)
在三个月的实际项目应用中,我总结了这些容易踩坑的细节:
时区问题:EDF头文件中的记录时间可能使用本地时区,需统一为UTC
python复制raw.set_meas_date(datetime.datetime.utcnow()) # 强制UTC
通道命名不一致:不同患者的电极位置可能有差异
python复制standard_names = {'FP1-F7':'Fp1', 'T7-P7':'T7'} # 映射表
raw.rename_channels(standard_names)
内存管理:大型EDF文件可能耗尽内存
python复制raw = mne.io.read_raw_edf(edf_path, preload=False) # 延迟加载
标注歧义:部分.seizure文件的开始时间可能相对于EDF起始
python复制# 需检查标注时间是否绝对
if 'start_time' in raw.info:
seizure_start += raw.info['start_time']
跨患者泛化:不同患者的EEG特征分布差异大,建议使用患者独立的标准化
python复制from sklearn.preprocessing import RobustScaler
scaler = RobustScaler().fit(X_train)
X_test = scaler.transform(X_test) # 不要fit_transform!
处理完第一批数据后,我习惯性检查数据质量矩阵:
| 质量指标 | 阈值 | 检查方法 |
|---|---|---|
| 信号丢失率 | <5% | (raw.get_data()==0).mean() |
| 通道相关性 | >0.7 | np.corrcoef(raw.get_data()) |
| 噪声功率比 | <30% | psd[:, freq>70Hz].sum() / psd.sum() |
| 发作标注覆盖率 | 100% | 比对.seizure与summary.txt |
这套流程在Kaggle癫痫预测竞赛中经过验证,单患者识别F1-score可达0.82。但要注意,直接套用其他EEG数据集时可能需要调整频带范围和滤波参数——毕竟大脑就像指纹,每个人的电活动模式都是独特的。