MNE-Python 第6天学习笔记:分段(Epoching)与基线校正

一、什么是分段(Epoching)?

1.1 通俗理解

到目前为止,我们处理的是连续的脑电数据(一整段录音)。

但脑电分析通常关注的是刺激出现前后的大脑反应

复制代码
连续数据:
├─────────────────────────────────────────────┤
0秒           100秒          200秒          300秒
     ↑ 刺激1       ↑ 刺激2       ↑ 刺激3

分段 = 把每个刺激前后的一小段时间"切"出来:

刺激1周围:  [刺激前0.2秒]──[刺激]──[刺激后0.5秒]
刺激2周围:  [刺激前0.2秒]──[刺激]──[刺激后0.5秒]
刺激3周围:  [刺激前0.2秒]──[刺激]──[刺激后0.5秒]

1.2 形象比喻

复制代码
你正在录一整天的声音:
  "......早上好......吃了吗......再见......"

分段就是:
  把每个"早上好"出现的前后各 1 秒切出来
  然后你就能分析:
    - "早上好"这个声音的平均波形是什么?
    - 每次说"早上好"有什么变化?

1.3 Epochs 数据结构

复制代码
Epochs 对象 = 三维数组

维度1:事件数量(比如 72 个听觉刺激 × 4 种条件 = 288 个分段)
维度2:通道数量(60 个 EEG 通道)
维度3:时间点数(比如 -0.2s 到 0.5s,共 421 个采样点)

形状:(288, 60, 421)
       ↑    ↑    ↑
     事件  通道  时间

二、环境准备与数据加载

2.1 导入库和基础设置

python 复制代码
# ========== 环境设置 ==========

# matplotlib.use('TkAgg') 的作用:
# 指定 matplotlib 的渲染后端
# TkAgg 是 Windows 系统自带的 Tkinter 后端
# 必须在 import pyplot 之前设置,否则可能会报错
# 常见后端对比:
#   TkAgg:Windows 自带,无需额外安装
#   Qt5Agg:功能更丰富,需要安装 PyQt5
#   Agg:不需要显示窗口,只保存图片(适合服务器)
import matplotlib
matplotlib.use('TkAgg')

# pyplot:matplotlib 的主要绘图接口
# 提供类似 MATLAB 的绘图函数(plot, subplot, title 等)
# plt 是约定俗成的简写
import matplotlib.pyplot as plt

# mne:脑电分析核心库
# 包含了数据加载、预处理、分析、可视化的所有功能
import mne

# numpy:数值计算库
# np 是约定俗成的简写
# 提供高效的数组操作、数学运算
import numpy as np

# os:操作系统路径处理模块
# 用于安全地拼接文件路径
import os

# Counter:统计工具
# 可以快速统计列表中各元素出现的次数
# 例如 Counter(['a','a','b']) → {'a':2, 'b':1}
from collections import Counter

# warnings:控制警告信息的显示
import warnings

# filterwarnings('ignore'):
# 忽略所有警告信息,让输出更干净
# 之前遇到的 Blowfish 警告、event missing 警告都会被隐藏
warnings.filterwarnings('ignore')

# ========== 中文字体设置 ==========

# rcParams 是 matplotlib 的全局配置字典
# 类似于软件的"设置"或"偏好设置"菜单

# 'font.sans-serif':设置无衬线字体列表
# matplotlib 会按列表顺序尝试,使用第一个可用的字体
# 'Microsoft YaHei' = 微软雅黑(Windows 常见,美观)
# 'SimHei' = 黑体(Windows 自带,备用方案)
plt.rcParams['font.sans-serif'] = ['Microsoft YaHei', 'SimHei']

# 'axes.unicode_minus':控制负号的显示方式
# True(默认):使用 Unicode 减号 "−"(更美观)
# False:使用 ASCII 连字符 "-"
# 为什么设为 False?
#   有些中文字体不支持 Unicode 减号
#   设为 False 可以避免负号显示为方块 □
plt.rcParams['axes.unicode_minus'] = False

# 打印标题分隔线
# "="*60 表示将 "=" 重复 60 次
print("="*60)
print("MNE-Python 第6天:分段(Epoching)与基线校正")
print("="*60)

2.2 加载原始数据

python 复制代码
# ---------- 1. 加载数据 ----------

# mne.datasets.sample.data_path() 的作用:
# 返回 MNE 示例数据集的存储路径
# 第一次运行时:
#   1. 自动从网上下载数据集(约 2GB)
#   2. 保存到 ~/mne_data/ 目录
# 之后运行时:直接返回本地路径,不会重新下载
sample_data_folder = mne.datasets.sample.data_path()

# os.path.join():安全地拼接文件路径
# 为什么不用 + 号直接拼接?
#   Windows 用反斜杠 \,Linux/Mac 用正斜杠 /
#   os.path.join() 会自动使用正确的分隔符
#   例如:
#     Windows: "MEG\sample\file.fif"
#     Linux:   "MEG/sample/file.fif"
raw_fname = os.path.join(
    sample_data_folder,        # 数据集根目录
    'MEG',                     # MEG 子目录
    'sample',                  # sample 子目录
    'sample_audvis_raw.fif'    # 原始数据文件名
)

# mne.io.read_raw_fif():读取 .fif 格式的原始脑电数据
# .fif = Functional Image File,MNE 的原生数据格式
#
# preload=False 的含义:
#   数据暂存在硬盘上,不加载到内存(RAM)
#   优点:节省内存,适合大文件
#   缺点:操作时需要从硬盘读取,速度慢
#   
# preload=True 的含义:
#   数据立即加载到内存
#   优点:操作速度快
#   缺点:占用内存(但这个数据集不大,可以接受)
raw = mne.io.read_raw_fif(raw_fname, preload=False)
print("✅ 原始数据加载完成")

.fif 文件内部结构:

python 复制代码
.fif 文件就像一个"大箱子",里面装有:
┌─────────────────────────────┐
│ 📊 信号数据                  │
│   - 每个通道每个时间点的数值  │
│                             │
│ 📋 通道信息                  │
│   - 通道名称(EEG 001 等)   │
│   - 通道类型(eeg/eog/stim) │
│   - 电极位置                 │
│                             │
│ ⚙️ 元数据                    │
│   - 采样率(600.61 Hz)      │
│   - 采集日期                 │
│   - 设备信息                 │
└─────────────────────────────┘

三、数据预处理

3.1 提取通道

python 复制代码
# ---------- 2. 快速预处理 ----------
print("\n快速预处理...")

# raw.copy():创建数据的独立副本
# 为什么要 copy?
#   保持原始 raw 对象不变
#   这样如果后续操作出错,可以回到原始状态重新开始
#   类似于"另存为"而不是"保存"

# pick_types():根据通道类型筛选通道
# eeg=True  → 保留 EEG(脑电)通道
# eog=True  → 保留 EOG(眼电)通道,后续 ICA 需要
# stim=True → 保留 STIM(刺激标记)通道,提取事件需要
# 
# 注意:这里没有 ecg=True,因为这个数据集没有 ECG 通道
raw_eeg = raw.copy().pick_types(eeg=True, eog=True, stim=True)
print(f"  提取了 {len(raw_eeg.ch_names)} 个通道")

通道类型说明:

3.2 重命名通道并设置 Montage

python 复制代码
# 找出所有以 'EEG' 开头的通道名称
# 列表推导式的语法:[表达式 for 变量 in 列表 if 条件]
# 遍历 raw_eeg.ch_names 中的每个通道名
# 只保留以 'EEG' 开头的
eeg_names = [ch for ch in raw_eeg.ch_names if ch.startswith('EEG')]
print(f"  EEG 通道数: {len(eeg_names)}")
print(f"  EEG 通道示例: {eeg_names[:3]}")  # 前3个,如 ['EEG 001', 'EEG 002', 'EEG 003']

# 创建标准 10-20 系统的蒙太奇(第2天学的内容)
# make_standard_montage() 返回一个 Montage 对象
# 包含 94 个标准电极的 3D 坐标
montage = mne.channels.make_standard_montage('standard_1020')

# 从标准蒙太奇中取相同数量的电极名称
# montage.ch_names 包含所有标准电极名(如 'Fz', 'Cz', 'Pz', ...)
# [:len(eeg_names)] 取前 N 个(N = EEG 通道数量)
standard_names = montage.ch_names[:len(eeg_names)]
print(f"  标准名称示例: {standard_names[:3]}")  # 如 ['Fz', 'Cz', 'Pz']

# dict(zip(A, B)) 详解:
# 步骤1:zip(eeg_names, standard_names)
#   将两个列表逐对配对,生成迭代器
#   → [('EEG 001', 'Fz'), ('EEG 002', 'Cz'), ('EEG 003', 'Pz'), ...]
#
# 步骤2:dict(...)
#   将配对列表转换为字典
#   → {'EEG 001': 'Fz', 'EEG 002': 'Cz', 'EEG 003': 'Pz', ...}
#
# 这个字典就是"重命名映射表"
rename_dict = dict(zip(eeg_names, standard_names))

# rename_channels():根据字典批量重命名通道
# 旧名称(键) → 新名称(值)
raw_eeg.rename_channels(rename_dict)
print(f"  重命名后通道示例: {raw_eeg.ch_names[:3]}")  # ['Fz', 'Cz', 'Pz']

# set_montage():设置电极在头皮上的位置
# MNE 会根据通道名称在 montage 中查找对应的 3D 坐标
# 这样才能画地形图
raw_eeg.set_montage(montage)
print("  通道重命名和蒙太奇设置完成")

3.3 🔑 关键步骤:先加载数据,再滤波

python 复制代码
# ⚠️ 重要:滤波等操作必须在数据加载到内存之后!

# load_data() 的作用:
# 将数据从硬盘文件读到计算机的内存(RAM)中
# 内存中的读写速度比硬盘快几百倍
# 
# 为什么不能在 preload=False 时做滤波?
#   滤波需要对每个数据点做数学运算
#   如果数据在硬盘上,每次读取都非常慢
#   MNE 为了防止这种情况,直接报错提醒你
print("  加载数据到内存...")
raw_eeg.load_data()
print("  ✅ 数据已加载到内存")

preload 与 load_data 的关系:

python 复制代码
# 方式1:加载时直接预加载
raw = mne.io.read_raw_fif('file.fif', preload=True)
# 此时数据已在内存中

# 方式2:先不加载,需要时再加载(本次的方式)
raw = mne.io.read_raw_fif('file.fif', preload=False)
# ... 做一些不需要内存的操作 ...
raw.load_data()  # 现在加载到内存
# ... 做需要内存的操作(滤波等)...

3.4 陷波滤波

python 复制代码
# notch_filter():陷波滤波器
# 
# 陷波滤波器在特定频率"挖一个非常窄的坑"
# 只去除这一个频率,不影响其他频率
#
# 50Hz(中国/欧洲)或 60Hz(美国)是交流电的频率
# 电线、电器产生的电磁场会被电极拾取
# 在脑电信号中表现为持续的嗡嗡声

print("  陷波滤波(去除 60Hz)...")
raw_eeg.notch_filter(
    freqs=60,           # 要去除的频率
    picks='eeg',        # 只对 EEG 通道滤波
    verbose=False       # 不打印详细信息
)
print("  ✅ 去除了 60Hz 工频干扰")

陷波滤波原理图:

python 复制代码
     信号幅度
        │
        │         ┌─┐
        │         │ │  ← 60Hz 的尖峰(工频干扰)
        │       ╱   ╲
        │      ╱     ╲___  ← 正常的脑电信号
        │     ╱
        └────────────────────→ 频率
              ↑
          60Hz被"挖掉"

3.5 带通滤波

python 复制代码
# filter():带通滤波器
# 
# 带通滤波 = 高通滤波 + 低通滤波
# 高通部分:去除低于 l_freq 的频率
# 低通部分:去除高于 h_freq 的频率
# 
# 本次的设置:
#   l_freq=1:去除 <1Hz 的缓慢漂移(出汗、电极移动)
#   h_freq=40:去除 >40Hz 的高频噪声(肌肉活动、电磁干扰)
#   保留 1-40Hz:包含了脑电的主要频段

print("  带通滤波(1-40 Hz)...")
raw_eeg.filter(
    l_freq=1,           # 低截止频率(高通部分)
    h_freq=40,          # 高截止频率(低通部分)
    picks='eeg',        # 只对 EEG 通道滤波
    verbose=False
)
print("  ✅ 保留了 1-40 Hz 的信号")

脑电频段与滤波的关系:

python 复制代码
频段         频率范围      本次是否保留
─────────────────────────────────────
Delta  δ     0.5-4 Hz      ✅ 保留(>1Hz 部分)
Theta  θ     4-8 Hz        ✅ 保留
Alpha  α     8-13 Hz       ✅ 保留
Beta   β     13-30 Hz      ✅ 保留
Gamma  γ     30-100 Hz     ⚠️ 部分保留(30-40Hz)
高频噪声     >40 Hz         ❌ 去除
基线漂移     <1 Hz          ❌ 去除

3.6 重参考

python 复制代码
# set_eeg_reference():设置 EEG 参考
# 
# 脑电测量的是"电位差":
#   记录值 = 电极电位 - 参考电位
# 
# 采集时可能以 Cz(头顶)或耳垂为参考
# 但这些位置本身也有脑电活动,不是"中性"的
#
# 平均参考('average'):
#   新参考 = 所有 EEG 通道的平均值
#   每个通道的新值 = 原值 - 所有通道的平均值
#   假设:全头平均电位 ≈ 0
#   优点:标准、可重复、适合高密度电极

print("  重参考(平均参考)...")
raw_eeg.set_eeg_reference(
    'average',          # 参考类型:所有通道的平均值
    verbose=False
)
print("  ✅ 重参考完成(平均参考)")

重参考的数学原理:

python 复制代码
# 假设有 3 个通道
ch1 = 10  # μV
ch2 = 20  # μV
ch3 = -5  # μV

# 平均参考
average = (10 + 20 + (-5)) / 3 = 8.33 μV

# 新的值
ch1_new = 10 - 8.33 = 1.67 μV
ch2_new = 20 - 8.33 = 11.67 μV
ch3_new = -5 - 8.33 = -13.33 μV

# 验证:新值的平均值 = 0
(1.67 + 11.67 + (-13.33)) / 3 ≈ 0  ✅

四、提取事件

4.1 从刺激通道提取事件

python 复制代码
# ---------- 3. 提取事件 ----------
print("\n提取事件...")

# find_events():从刺激通道自动检测事件
# 
# 工作原理:
#   1. 监控刺激通道(STI 014)的值
#   2. 平时值是 0,刺激出现时值变为非 0
#   3. 检测到变化时,记录为事件
#
# 返回值:events 数组
#   形状:(N, 3)
#   N = 事件总数
#   每行 = [采样点编号, 持续采样点数, 事件编号]

events = mne.find_events(
    raw_eeg,                   # 包含刺激通道的数据
    stim_channel='STI 014'     # 刺激通道的名称
)
print(f"  提取到 {len(events)} 个事件")

# 查看事件数组的前5行
print(f"\n  前5个事件:")
print(f"    采样点    持续时间  事件编号")
for i in range(min(5, len(events))):
    sample = events[i, 0]      # 第0列:采样点位置
    duration = events[i, 1]    # 第1列:持续时间
    event_code = events[i, 2]  # 第2列:事件编号
    time_sec = sample / raw_eeg.info['sfreq']  # 转换为秒
    print(f"    {sample:6d}    {duration:4d}       {event_code}  (={time_sec:.1f}秒)")

事件数组的结构:

python 复制代码
# events 是一个 N×3 的二维数组
# 可以理解为一张表格:

# ┌─────────┬─────────┬─────────┐
# │ 采样点  │ 持续时间│ 事件编号│
# ├─────────┼─────────┼─────────┤
# │  15000  │    0    │    1    │ ← 第1个事件:编号1,瞬时
# │  23000  │    0    │    2    │ ← 第2个事件:编号2,瞬时
# │  31000  │    0    │    1    │ ← 第3个事件:编号1,瞬时
# │  ...    │   ...   │   ...   │
# └─────────┴─────────┴─────────┘

4.2 创建事件 ID 字典

python 复制代码
# event_id 字典:将数字编号映射为有意义的事件名称
# 键 = 条件名称(字符串)
# 值 = 事件编号(整数)

event_id = {
    '听觉/左耳': 1,      # 编号 1 = 左耳听到纯音
    '听觉/右耳': 2,      # 编号 2 = 右耳听到纯音
    '视觉/左眼': 3,      # 编号 3 = 左眼看到棋盘格
    '视觉/右眼': 4       # 编号 4 = 右眼看到棋盘格
}

# 统计每种事件的次数
print("\n事件统计:")
for event_name, event_code in event_id.items():
    # .items() 返回 (键, 值) 对
    # np.sum(events[:, 2] == event_code)
    #   统计第2列中等于 event_code 的行数
    count = np.sum(events[:, 2] == event_code)
    print(f"  {event_name} (编号{event_code}): {count} 次")

五、创建 Epochs(分段)

5.1 理解时间窗口

python 复制代码
print("""
分段的时间窗口设计:

           刺激前(基线期)    刺激点    刺激后(反应期)
    ←──────────|──────────★──────────|──────────→
   -0.2秒       0秒                           0.5秒
   
   tmin = -0.2:从刺激前 0.2 秒开始
   tmax = 0.5:到刺激后 0.5 秒结束
   
   为什么这样设置?
   - 基线期(-0.2 ~ 0秒):观察刺激前的大脑状态
   - 反应期(0 ~ 0.5秒):观察刺激引起的大脑反应
   - 总长 0.7 秒:足够看到早期和中期脑电成分
""")

5.2 定义拒绝标准

python 复制代码
# reject 参数:定义"坏段"的判断标准
# 
# 什么是"坏段"?
#   幅度异常大的分段,通常是由残留伪迹引起的:
#     - 被试突然咳嗽
#     - 电极瞬间接触不良
#     - 眨眼没有被 ICA 完全去除
# 
# reject_criteria 是一个字典
# 键 = 通道类型
# 值 = 幅度阈值(单位:伏特)
#
# 150e-6 的含义:
#   150 × 10⁻⁶ = 0.000150 伏特 = 150 微伏
#   如果分段中任何 EEG 通道的幅度超过 ±150μV
#   这个分段就会被丢弃

reject_criteria = dict(
    eeg=150e-6  # 150 微伏
)
print(f"  拒绝标准: 幅度超过 {reject_criteria['eeg']*1e6:.0f} 微伏的分段将被丢弃")

不同伪迹的幅度参考:

5.3 创建 Epochs 对象

python 复制代码
# ---------- 4. 创建 Epochs ----------
print("\n创建 Epochs...")

# mne.Epochs():从连续数据创建分段
epochs = mne.Epochs(
    # ── 数据源 ──
    raw_eeg,                    # 连续 EEG 数据
    events,                     # 事件数组(告诉 MNE 在哪切)
    event_id=event_id,          # 事件 ID 字典(告诉 MNE 每个编号叫什么)
    
    # ── 时间窗口 ──
    tmin=-0.2,                  # 分段起始时间(刺激前 0.2 秒)
    tmax=0.5,                   # 分段结束时间(刺激后 0.5 秒)
    # 总时长 = tmax - tmin = 0.5 - (-0.2) = 0.7 秒
    
    # ── 基线校正 ──
    baseline=(-0.2, 0),         # 用刺激前 0.2 秒做基线
    # 原理:每个分段减去基线期的平均值
    # 效果:让所有分段从同一"起跑线"开始
    
    # ── 质量控制 ──
    reject=reject_criteria,     # 拒绝幅度异常的分段
    
    # ── 内存管理 ──
    preload=True,               # 加载到内存,加速后续操作
    
    # ── 输出控制 ──
    verbose=False               # 不打印详细信息
)

# 输出统计信息
print(f"✅ Epochs 创建完成")
print(f"  总事件数: {len(events)}")
print(f"  保留分段: {len(epochs)} ({len(epochs)/len(events)*100:.1f}%)")
print(f"  丢弃分段: {len(events) - len(epochs)} ({(len(events)-len(epochs))/len(events)*100:.1f}%)")

# 查看各条件的分段数
print(f"\n各条件分段统计:")
for cond_name in event_id.keys():
    n_epochs = len(epochs[cond_name])
    n_events = np.sum(events[:, 2] == event_id[cond_name])
    print(f"  {cond_name}: {n_epochs}/{n_events} 个 (保留率 {n_epochs/n_events*100:.1f}%)")

Epochs 参数详解表:

六、基线校正详解

6.1 为什么需要基线校正?

python 复制代码
问题:不同分段可能在不同的"高度"

分段1:  ~~~~~/‾‾‾‾‾~~~~     ← 整体偏上(+20 μV 偏移)
分段2:  ___/‾‾‾‾‾‾___     ← 整体偏下(-10 μV 偏移)
分段3:  ~~~~~/‾‾‾‾‾~~~~     ← 整体居中(0 μV)

如果不校正,叠加平均会被偏移污染
就好像把不同亮度的照片叠在一起,结果一片模糊

基线校正后:
分段1:  ___/‾‾‾‾‾‾___     ← 都从 0 开始
分段2:  ___/‾‾‾‾‾‾___     ← 都从 0 开始
分段3:  ___/‾‾‾‾‾‾___     ← 都从 0 开始

现在可以公平地比较和叠加了!

6.2 基线校正的数学原理

python 复制代码
# 基线校正的数学过程:

# 对于每个分段、每个通道:
# 1. 找到基线期的时间范围(-0.2 到 0 秒)
# 2. 计算基线期内的平均值
#    baseline_mean = mean(data[基线期内])
# 3. 用整个分段减去这个平均值
#    data_corrected = data - baseline_mean
# 4. 结果:基线期的平均值为 0

# 简单例子:
原始分段 = [2.1, 2.2, 2.0, 2.5, 2.8, 3.1, 3.5]
#          ↑── 基线期 ──↑  ↑── 反应期 ──↑
基线均值 = (2.1 + 2.2 + 2.0) / 3 = 2.1
校正后 = [-0.0, 0.1, -0.1, 0.4, 0.7, 1.0, 1.4]
#         ↑── 基线期 ≈ 0 ──↑

七、Epochs 可视化

7.1 蝴蝶图

python 复制代码
# ---------- 5. 可视化 ----------
print("\n可视化...")

# epochs.plot():绘制分段的"蝴蝶图"
# 
# 为什么叫蝴蝶图?
#   所有分段画在同一张图上
#   像蝴蝶翅膀一样展开
#
# 怎么看?
#   每条半透明的线 = 一个分段
#   线条的分布 = 不同分段之间的差异
#   线条密集处 = 稳定的脑电反应
#   线条离散处 = 变化较大的脑电反应

print("1. 绘制前 20 个分段的蝴蝶图...")
epochs[:20].plot(
    n_epochs=20,        # 显示的分段数量
    n_channels=10,      # 显示的通道数量
    scalings='auto',    # 自动调整纵轴比例
    show=True           # 显示图形
)
plt.suptitle('前 20 个分段(蝴蝶图)', fontsize=14, fontweight='bold')
plt.show(block=True)    # block=True:保持窗口打开

蝴蝶图解读指南:

python 复制代码
    振幅
      ↑
      │  ╱‾‾╲        ← 所有分段在刺激后的反应
      │ ╱    ╲___     
      │╱        ╲___  
  ────┼──────────────→ 时间
      │    ↑
      │  刺激点(0秒)
      
  看什么?
  1. 基线期(-0.2~0秒)应该接近 0
  2. 刺激后各分段的变化方向是否一致
  3. 有没有特别离谱的分离(可能是坏段)

7.2 丢弃日志

python 复制代码
# plot_drop_log():绘制分段丢弃日志
# 
# 这个图显示:
#   - 每个条件有多少分段被丢弃
#   - 丢弃的原因是什么
#   - 被丢弃分段之间的关联

print("\n2. 绘制分段丢弃日志...")
fig = epochs.plot_drop_log(show=False)
plt.suptitle('分段丢弃日志', fontsize=14, fontweight='bold')
plt.show(block=True)

八、保存结果

python 复制代码
# ---------- 6. 保存 ----------

# save():将 Epochs 对象保存为 .fif 文件
# 保存了什么?
#   - 所有分段的数据
#   - 通道信息
#   - 事件信息
#   - 时间信息
# 下次可以直接加载,无需重新预处理和分段
epochs.save('day6_epochs.fif', overwrite=True)
print("\n✅ 数据已保存为 day6_epochs.fif")

print("\n" + "="*60)
print("第6天学习完成!")
print("="*60)

九、第6天完整代码

python 复制代码
# ========== 环境设置 ==========
import matplotlib
matplotlib.use('TkAgg')
import matplotlib.pyplot as plt
import mne
import numpy as np
import os
from collections import Counter
import warnings
warnings.filterwarnings('ignore')

plt.rcParams['font.sans-serif'] = ['Microsoft YaHei', 'SimHei']
plt.rcParams['axes.unicode_minus'] = False

print("="*60)
print("MNE-Python 第6天:分段(Epoching)与基线校正")
print("="*60)

# ---------- 1. 加载数据 ----------
sample_data_folder = mne.datasets.sample.data_path()
raw_fname = os.path.join(sample_data_folder, 'MEG', 'sample', 'sample_audvis_raw.fif')
raw = mne.io.read_raw_fif(raw_fname, preload=False)
print("✅ 原始数据加载完成")

# ---------- 2. 快速预处理 ----------
print("\n快速预处理...")
raw_eeg = raw.copy().pick_types(eeg=True, eog=True, stim=True)
eeg_names = [ch for ch in raw_eeg.ch_names if ch.startswith('EEG')]
montage = mne.channels.make_standard_montage('standard_1020')
standard_names = montage.ch_names[:len(eeg_names)]
raw_eeg.rename_channels(dict(zip(eeg_names, standard_names)))
raw_eeg.set_montage(montage)

print("  加载数据到内存...")
raw_eeg.load_data()

print("  陷波滤波(去除 60Hz)...")
raw_eeg.notch_filter(freqs=60, picks='eeg', verbose=False)

print("  带通滤波(1-40 Hz)...")
raw_eeg.filter(l_freq=1, h_freq=40, picks='eeg', verbose=False)

print("  重参考(平均参考)...")
raw_eeg.set_eeg_reference('average', verbose=False)
print("✅ 快速预处理完成")

# ---------- 3. 提取事件 ----------
print("\n提取事件...")
events = mne.find_events(raw_eeg, stim_channel='STI 014')
print(f"  提取到 {len(events)} 个事件")

event_id = {
    '听觉/左耳': 1,
    '听觉/右耳': 2,
    '视觉/左眼': 3,
    '视觉/右眼': 4
}

for event_name, event_code in event_id.items():
    count = np.sum(events[:, 2] == event_code)
    print(f"  {event_name}: {count} 次")

# ---------- 4. 创建 Epochs ----------
print("\n创建 Epochs...")
reject_criteria = dict(eeg=150e-6)

epochs = mne.Epochs(
    raw_eeg, events, event_id=event_id,
    tmin=-0.2, tmax=0.5,
    baseline=(-0.2, 0),
    reject=reject_criteria,
    preload=True,
    verbose=False
)

print(f"✅ Epochs 创建完成")
print(f"  总事件: {len(events)}, 保留: {len(epochs)}, 丢弃: {len(events)-len(epochs)}")

# ---------- 5. 可视化 ----------
print("\n可视化...")
epochs[:20].plot(n_epochs=20, n_channels=10, scalings='auto', show=True)
plt.suptitle('前 20 个分段(蝴蝶图)', fontsize=14)
plt.show(block=True)

fig = epochs.plot_drop_log(show=False)
plt.suptitle('分段丢弃日志', fontsize=14)
plt.show(block=True)

# ---------- 6. 保存 ----------
epochs.save('day6_epochs.fif', overwrite=True)
print("\n✅ 数据已保存为 day6_epochs.fif")

print("\n" + "="*60)
print("第6天学习完成!")
print("="*60)

十、今日总结

📝 核心概念

🛠️ 掌握的技能

🔑 操作顺序铁律

python 复制代码
1. 提取通道        pick_types()
2. 重命名通道      rename_channels()
3. 设置蒙太奇      set_montage()
4. 加载到内存      load_data()      ← 关键!必须在这之后
5. 陷波滤波        notch_filter()
6. 带通滤波        filter()
7. 重参考          set_eeg_reference()
8. 提取事件        find_events()
9. 创建分段        mne.Epochs()
相关推荐
SilentSamsara6 小时前
concurrent.futures 实战:进程池与线程池的统一抽象
运维·开发语言·python·青少年编程
水木流年追梦7 小时前
大模型入门-大模型的推理策略
开发语言·python·算法·正则表达式·prompt
Cthy_hy7 小时前
Python 算法竞赛:数学核心知识点全总结
python·算法
独隅7 小时前
DeepSpeed ZeRO-3在TensorFlow中缺失的底层支持机制与优化全面指南
人工智能·python·tensorflow
松☆7 小时前
昇腾NPU上的张量操作库,和PyTorch的张量操作有啥不一样?
人工智能·pytorch·python
PILIPALAPENG8 小时前
第4周 Day 4:Agent 工作流模式——编排复杂流程
前端·人工智能·python
EnCi Zheng9 小时前
09a-斯坦福 CS336 作业一:BPE分词器
开发语言·python·算法
5201-9 小时前
Cube MatMul:为什么矩阵乘法选了 Cube 而不是 Vector
pytorch·python·矩阵
weixin_448946639 小时前
安裝Hermes
python