ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

Python实战:从时域到频域,系统掌握EEG脑电数据分析方法

Python实战:从时域到频域,系统掌握EEG脑电数据分析方法 拿到一份EEG数据的时候很多人第一反应是懵的。几十个通道、几十万行数字画出来像一堆失控的股票曲线——这真的能看出大脑在想什么答案是能但前提是你得学会从两个视角去看同一份数据时域告诉你信号在什么时间点发生了什么频域告诉你信号里藏着哪些频率成分。这篇实战文章就围绕这两条线展开用Python把脑电数据的读取、清洗、特征提取和频带分析完整走一遍适合刚接触EEG分析、想知道数据拿到手之后到底该干什么的初学者也适合做过一些处理但想补上频域这块短板的同学。1. 从原始脑电数据到有用信号先搞懂这几个关键概念在敲第一行代码之前有几个概念值得先弄清楚。EEG脑电图记录的是大脑皮层大量神经元同步放电产生的微弱电位变化幅值通常在微伏μV级别比心电信号还弱一个数量级。正因为信号微弱它极其容易被眼电、肌电、工频干扰等污染这也是EEG分析里预处理比分析本身更耗时的根本原因。1.1 采样率、电极与数据维度采样率决定了数据的时间分辨率。根据奈奎斯特采样定理采样率至少要是信号最高频率的两倍否则就会发生混叠——高频成分会伪装成低频成分混进数据里。临床EEG常用的采样率有250Hz、500Hz、1000Hz如果你关注的最高频段是Gamma波30-50Hz250Hz的采样率理论上够用但实际工程中一般会留足余量至少5倍以上。电极的分布遵循国际10-20系统名字里的F代表额叶、T代表颞叶、C代表中央区、P代表顶叶、O代表枕叶奇数在左半球、偶数在右半球、z代表中线位置。拿到数据第一步先确认通道名称和排列顺序因为不同的数据导出格式EDF、BDF、CSV通道排列规则可能不一样后面做坏道检测和空间分析时通道顺序错了会直接导致结果对不上号。数据维度是另一个常被忽略的点。一个典型的数据文件长这样数据形状 通道数 × 采样点数比如32通道记录10秒、采样率250Hz就是32 × 2500。注意这个顺序不是采样点数在前而是通道数在前。处理时转置搞错轻则报错重则画出来的图根本没法看。1.2 时域和频域到底在说什么时域分析关心的是信号幅度随时间怎么变——比如某个时间点出现了明显的尖峰那是眨眼伪迹某段信号幅度突然变大那可能是肌肉紧张。频域分析关心的则是信号中有哪些频率成分、各自有多强——Alpha波8-13Hz在闭眼放松时增强Beta波13-30Hz在专注时增强这些信息在时域波形上很难一眼看出来但一转到频域就清清楚楚。用个生活化类比时域看的是整首乐曲的波形播放过程频域看的是这首曲子的频谱——有哪些音高、各自音量多大。同一个信号只是换个视角观察数学工具就是傅里叶变换。2. Python分析环境搭建这次我把踩过的坑都写出来EEG分析的Python生态核心是mne这个库它封装了数据读取、预处理、事件分段、频谱分析的一整套流程。但很多入门教程一上来就让用户装mne结果不少人挂在第一步。这里从最基础的依赖讲起。2.1 库的安装与选择需要安装的东西一共就四个numpy做数值计算scipy提供信号处理函数滤波、傅里叶变换matplotlib画图mne处理EEG专用格式EDF、BDF、BrainVision等。pip install numpy scipy matplotlib mne如果你在国内网络环境下装得慢可以用清华源加速pip install numpy scipy matplotlib mne -i https://pypi.tuna.tsinghua.edu.cn/simplemne这个库比较重因为它依赖numba第一次导入会编译缓存可能需要十几秒甚至更久这不是卡死了是正常的。如果你机器内存有限导入mne之后再设置一下这个参数能省不少内存import mne mne.set_log_level(WARNING)把INFO级别的日志关掉不仅控制台清爽处理大数据时也能省掉大量控制台输出的开销。2.2 为什么推荐用EDF格式而不是CSV我见过太多人拿CSV格式的EEG数据来找我排查问题——分析结果怎么都跟文献对不上。EDFEuropean Data Format是脑电采集设备的通用格式它把采样率、通道名称、增益、时间戳这些元信息全部内嵌在文件里读取时不需要你手动填参数最大限度避免了采样率写错导致频率轴全错的灾难。用mne读取EDF文件代码非常简单raw mne.io.read_raw_edf(subject01.edf, preloadTrue) print(raw.info) # 查看采样率、通道信息 print(raw.ch_names) # 查看通道名列表 print(raw.get_data().shape) # 数据形状如果是CSV数据你得自己维护采样率、通道名、单位换算这些信息任何一个地方出错都会在后面的分析里以诡异的方式体现出来。涉及的额外提醒如果你的数据是从商业设备导出的先确认设备厂商是否提供了Python读取库或导出插件。常见的厂商如Neuroscan、Brain Products、Biosemi都支持导出EDF或BDF格式优先用EDF握手不要手动拼接原始数据。3. 时域分析实战坏道检测、滤波与特征提取时域分析的第一步永远是画图让眼睛先扫一遍数据。这一步不是走过场它是后续一切分析的质量关卡——坏道不清理频域算得再花哨也没意义。3.1 用肉眼找问题绘制原始波形用mne画波形最长用的是raw.plot()它是交互式的可以缩放、拖动、切换通道。如果你是新手建议先把全部通道压缩在一张图里看宏观趋势raw.plot(n_channels32, duration10, scalingsauto, blockTrue)重点关注这几类现象某一通道全部是一条直线说明该通道已坏死。某一通道幅度远大于其他通道比如超过1mV可能是电极接触不良或硬件饱和。所有通道都有的周期性尖峰通常是50Hz工频干扰国内交流电频率。低频大波浪漂移通常是基线漂移或受试者出汗导致的电极极化。这些在时域波形上识别出来后后续处理就很有针对性。3.2 坏道检测的量化指标肉眼扫完还需要量化指标来支撑判断。这里介绍一下我在实际项目中用的三个指标综合起来判坏道不依赖单一指标一个常见思路是用全通道标准差画分布图某个通道标准差与其他通道差异过大约超出3倍四分位距就标记为可疑坏道。mne还提供了mne.preprocessing.find_bad_channels它的原理是根据通道之间的相关性来排查异常——如果一个通道与相邻通道的相关性显著低于正常水平就可能有问题。我自己常用的坏道判据如下指标正常范围疑似坏道判断峰峰值peak-to-peak一般远小于1000μV单通道峰峰值超过±500μV且不是因为正常眨眼标准差多数通道集中分布标准差为0平线或与全通道中位数相差3倍以上与相邻通道的皮尔逊相关系数一般大于0.5相关系数低于0.2或为负检测到坏道后的常用做法是插值修复mne提供了一步到位的方案raw.info[bads] [Fz, O1] # 手动标记坏道 raw raw.interpolate_bads() # 用周围通道插值重建插值不是万能的如果坏道数量超过总通道数的30%插值结果基本不可信这种情况建议直接丢弃这段数据或者补充记录。3.3 滤波处理去除工频干扰和基线漂移EEG的滤波策略一般是带通陷波。带通滤波器保留有效脑电波段比如0.5-40Hz滤掉超低频漂移和高频肌电噪声陷波滤波器专门干50Hz工频干扰。这里有一个重要选择用filtfilt而不是lfilter。filtfilt是零相位滤波它正向滤波一遍再反向滤波一遍相位偏移相互抵消信号波形不会被推移。lfilter会引入相位延迟在分析事件相关电位时是致命问题。使用scipy实现带通滤波from scipy.signal import butter, filtfilt def bandpass_filter(data, fs, low0.5, high40, order4): b, a butter(order, [low, high], btypebandpass, fsfs) return filtfilt(b, a, data, axis-1) # 按通道滤波 data raw.get_data() # 形状: 通道数 × 采样点数 data_filt bandpass_filter(data, fs250)陷波滤波用iirnotch品质因子Q一般取30左右from scipy.signal import iirnotch b, a iirnotch(50, Q30, fs250) data_notched filtfilt(b, a, data_filt, axis-1)滤波顺序我习惯先陷波再带通。因为陷波滤波器的频率响应在50Hz附近会有副作用如果先用带通把带外能量压制掉陷波时数值运算更稳定。3.4 时域特征提取滤波干净之后时域分析的价值在于提取一些简单但好用的特征。大致分几类统计特征每个通道的均值、方差、标准差、峰峰值。复杂度特征过零率signal中穿越零点的次数/总点数、Hjorth参数活动度、移动度、复杂度。事件相关特征锁时time-locked的均值波形成分。过零率虽然基础但对脑电状态的区分度很不错注意力松散的Alpha波段占比高信号振荡感强过零率会偏低注意力集中时Beta波占优过零率相对升高。Hjorth参数可以直接用mne提取import mne # 假设已经分段成二维的时代数据 epochs mne.Epochs(raw, events, tmin-0.2, tmax0.8, baseline(None, 0), preloadTrue) features mne.time_frequency.compute_epochs_hjorth_spectral_power(epochs)Hjorth特征计算量小、容易解释非常适合作为快速分类的输入特征。4. 频域分析实战从傅里叶变换到功率谱频域分析的核心工具是傅里叶变换。它的思想并不难任何一段信号都可以分解成不同频率正弦波的叠加。把信号变换到频域后横轴是频率纵轴是该频率成分的强度。4.1 快速傅里叶变换FFT的正确打开方式用scipy对一段EEG做FFTfrom scipy.fft import fft, fftfreq import numpy as np fs 250 # 采样率 data_ch data_filt[0, :] # 取某一个通道 N len(data_ch) y fft(data_ch) freqs fftfreq(N, 1/fs) # 只保留单边频谱正频率部分幅值要乘以2除DC分量外 half N // 2 mag 2.0 * np.abs(y[:half]) / N freqs_half freqs[:half]这里有一个高频踩坑点fft的结果是复数取绝对值才能得到幅值谱。单边频谱的幅值要乘以2直流分量即0Hz处不乘。如果忘记这个乘以2频谱幅值会全部偏小按绝对幅值对比文献或者不同被试时数据根本对不上。频率分辨率由fs/N决定。采样率250Hz取2秒数据500点分辨率是0.5Hz取1秒数据分辨率是1Hz。换句话说想分辨出9.5Hz和10Hz的峰至少要2秒的窗长这是物理限制补零无法突破。4.2 用Welch方法计算功率谱密度直接对整段数据做FFT得到的频谱噪声很大因为脑电是非平稳信号。实际工程中更常用的是Welch方法把长信号切成多段每段加窗做FFT再把所有段的谱平均。这样做以牺牲频率分辨率为代价换来的是更平滑、更稳定的功率谱估计。mne里封装了compute_psd非常方便from mne.time_frequency import compute_psd_welch psd compute_psd_welch(raw, tmin0, tmax10, fmin0.5, fmax50, n_fft500, n_overlap250) psd.plot() # 查看功率谱参数解读n_fft500表示每个窗取500点2秒频率分辨率0.5Hzn_overlap250表示段与段之间重叠50%重叠能增加段数让平均的降噪效果更好。如果用纯scipy实现是这样的from scipy.signal import welch freqs, psd welch(data_ch, fs250, nperseg500, noverlap250, windowhann)窗函数选hann是脑电频谱分析里最常见的选择它主瓣宽一点但旁瓣衰减大低频段不容易被泄漏污染。4.3 频带划分与节律波分析EEG频谱的标志性特征是节律波典型的频带划分如下频带频率范围典型状态Delta0.5-4Hz深睡眠Theta4-8Hz困倦、冥想Alpha8-13Hz清醒放松、闭眼Beta13-30Hz思考、警觉Gamma30-50Hz高度认知加工实际分析中最常用的特征是各频带的相对功率——某频带功率占总功率的比例。因为EEG绝对功率受颅骨厚度、电极阻抗等因素影响很大不同被试之间绝对功率不可比但相对功率可以作为状态指标跨被试比较。计算各频带相对功率的代码def band_power(psd, freqs, band, fs): idx np.logical_and(freqs band[0], freqs band[1]) return np.sum(psd[idx]) bands {Delta: (0.5, 4), Theta: (4, 8), Alpha: (8, 13), Beta: (13, 30)} total_power np.sum(psd) for name, band in bands.items(): bp band_power(psd, freqs, band, fs) print(f{name}: {bp / total_power * 100:.2f}%)注意频带边界怎么选没有绝对标准不同文献略有出入。写报告或论文时一定要注明自己用的频带划分标准否则结果无法复现。5. 一个完整的小案例睁眼闭眼状态的Alpha波对比理论部分讲完用一个小案例把时域和频域的技术串起来。经典的睁眼闭眼EO/EC实验被试闭眼放松时枕区Alpha波8-13Hz功率显著增高睁眼后Alpha波被抑制。这个效应在频域上一眼就能看到。因为真实采集数据不是人人都有条件这里先用模拟数据演示完整流程代码逻辑同样适用于真实EEG数据。5.1 数据模拟与流程设计模拟一段10秒、250Hz采样率、单通道的信号包含以下成分10Hz的Alpha振荡闭眼阶段幅值比睁眼阶段高0.5Hz基线漂移50Hz工频干扰随机高斯噪声import numpy as np from scipy import signal from scipy.signal import welch, butter, filtfilt, iirnotch fs 250 t_total 10 t np.arange(0, t_total, 1/fs) N len(t) # 基底信号闭眼前5秒Alpha强后5秒Alpha弱 alpha_freq 10 alpha_env np.zeros(N) alpha_env[t 5] 30e-6 # 闭眼阶段 Alpha 幅值较大30μV alpha_env[t 5] 8e-6 # 睁眼阶段 Alpha 幅值较小8μV alpha_wave alpha_env * np.sin(2 * np.pi * alpha_freq * t) # 漂移 工频 噪声 drift 50e-6 * np.sin(2 * np.pi * 0.5 * t) noise 10e-6 * np.random.randn(N) powerline 20e-6 * np.sin(2 * np.pi * 50 * t) data alpha_wave drift powerline noise5.2 完整处理链路滤波、分段、频谱对比# 滤波带通 0.5-40Hz 陷波 50Hz b, a butter(4, [0.5, 40], btypebandpass, fsfs) data_filt filtfilt(b, a, data) b2, a2 iirnotch(50, Q30, fsfs) data_clean filtfilt(b2, a2, data_filt) # 分段前5秒和后5秒分别作为一个状态 seg_ec data_clean[t 5] # 闭眼 seg_eo data_clean[t 5] # 睁眼 # Welch功率谱 freqs_ec, psd_ec welch(seg_ec, fsfs, nperseg500, noverlap250) freqs_eo, psd_eo welch(seg_eo, fsfs, nperseg500, noverlap250) # Alpha频带功率 idx_alpha np.logical_and(freqs_ec 8, freqs_ec 13) alpha_power_ec np.sum(psd_ec[idx_alpha]) alpha_power_eo np.sum(psd_eo[idx_alpha]) print(f闭眼 Alpha波段功率: {alpha_power_ec:.3e}) print(f睁眼 Alpha波段功率: {alpha_power_eo:.3e}) print(f闭眼/睁眼 Alpha功率比: {alpha_power_ec / alpha_power_eo:.2f})按这个模拟参数走一遍闭眼段Alpha功率和睁眼段会有数量级的差异。再把两个状态的频谱叠画在一张图上会看到非常直观的现象8-13Hz区间闭眼曲线明显高于睁眼曲线这就是Alpha抑制效应在频域上的体现。5.3 结果解读与边界提醒这个经典效应背后有几个值得注意的边界条件。Alpha波在枕区O1、O2、Oz导联最明显本例用的是单个代表通道如果你处理真实数据建议直接把O开头通道提取出来单独分析而不是用全脑平均。瞳孔状态、精神状态、药物影响都会改变Alpha绝对功率所以单靠一次测量就下结论是危险的。实际科研中要有足够多的试次做统计检验而不是只看两条曲线的面积差。6. 实操中容易翻车的几个细节最后聊几个我在实际处理中踩过、也帮别人排查过的坑都很隐蔽但每一个都能让分析结果彻底跑偏。6.1 频率分辨率与零填充的认知误区很多人以为对FFT做zero padding补零能提高分辨率这是误解。补零只是对频谱做了插值让曲线看起来更平滑但真实可区分两个相邻频率的能力由原始信号长度决定公式是Δf fs / N_original。一段1秒的数据补零到10秒长度频谱上看起来点了更密但10Hz和10.5Hz还是分不开。不要再被某些教程误导了高频时间上的一个真正分辨率是记录时长不是FFT点数。6.2 滤波的边界效应与短数据段filtfilt在边界处有处理逻辑但短数据段仍然会被边界效应污染。比如你有一批每个epoch只有200ms的短片段200ms250Hz就是50个采样点这时候做0.5Hz高通滤波就很不合理——0.5Hz的波长是2秒窗口里连一个完整波长都没有滤波结果基本是废的。我的习惯是但凡做频域分析数据段长度至少是目标最低频率的5倍波长。要分析1Hz成分至少取5秒如果片段太短就放宽高通截止频率到2Hz或3Hz宁可丢掉慢波信息也不能让滤波把残存的低频伪影涂到全段。6.3 幅值单位与基线校正EEG设备导出的原始数据单位五花八门有的直接在文件里是μV有的以V为单位有的用一个整数编码加一个增益系数才能还原为物理量。mne读取EDF时会自动换算成V1V 1e6 μV很多国产软件导出的单位却是μV。做对比分析之前务必确认通道单位统一否则频谱幅值差6个数量级图表全都失真。基线校正在事件相关电位分析中几乎是必做的。原理是拿事件发生前的一段时间例如-200ms到0作为基线从信号中减去这段时间的均值目的是消除不同试次之间的直流偏置和低频漂移。mne中直接传baseline(None, 0)就会自动处理。6.4 陷波滤波器的副作用陷波滤波器能去掉50Hz工频但它也会把50Hz附近的有用信号一并削弱。尤其当你在研究Gamma频段30-50Hz时陷波会直接伤及Lambda波高频视觉诱发电位。这种情况下更推荐的做法是分析前先看频谱如果50Hz处没有明显尖峰就不做陷波。如果做了陷波不要把分析频段上限设置到48Hz以上。原始记录时尽量用参考电极做在线工频抑制很多采集设备有此设置后期处理只用带通滤波就足够。收尾前的最后建议项目做到中后期我回头看最大的感触是**频域分析的质量上限在时域预处理阶段就定死了。**坏道没清、伪迹没剔、滤波顺序错了后面算出来的功率谱越漂亮越可疑。所以我给新手的建议一直是——先花70%的精力把时域的质量控制做扎实再考虑上频谱分析。时间序列数据还有一个常被忽略的点画图时先用低频滤波后的数据快速看一眼整体趋势再用原始数据检查高频噪声两个视角来回切换很多问题一眼就有数。脑电分析的进阶方向还有不少可挖时频分析短时傅里叶变换、小波变换能看频率随时间的变化微状态分析关注空间模式的时序切换连通性分析能刻画通道之间的信息流动。这些都是在时域→频域这条主线的自然延伸——把基础打牢后面的路会顺很多。
返回列表