MNE-Python 第3天学习笔记:事件与标记处理

一、什么是"事件"?

1.1 通俗理解

想象你在做一个实验:

👂 听到"嘀"声 → 这是事件 1

👂 听到"嘟"声 → 这是事件 2

👁️ 看到闪光 → 这是事件 3

每一次刺激就是一个**"事件"** 。在脑电数据中,事件就是记录实验过程中发生了什么、什么时候发生的标记

1.2 事件在数据中的存储

复制代码
原始数据:
EEG通道1:  [0.1, 0.2, 0.1, 0.3, 0.2, ...]  ← 脑电波
EEG通道2:  [0.2, 0.1, 0.3, 0.2, 0.1, ...]  ← 脑电波
  ...
刺激通道:  [0, 0, 0, 1, 0, 0, 0, 2, 0, ...]  ← 事件标记
                     ↑           ↑
                 事件1发生    事件2发生

刺激通道(STIM) 平时是 0,事件发生时变成对应的编号。

1.3 事件的三个要素

二、实战:探索数据中的事件

2.1 ⚠️ 重要经验:先探索,后定义

很多教程直接告诉你事件编号是 1、2、3、4,但实际数据往往更复杂。正确的做法是:

复制代码
先查看数据中有哪些事件编号 → 再创建 event_id 字典

2.2 完整代码:环境准备

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 第3天:事件与标记处理")
print("="*60)

# 加载数据
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("✅ 数据加载完成")

代码解析:

三、查看刺激通道

3.1 找到刺激通道

python 复制代码
print("\n" + "="*60)
print("查看刺激通道")
print("="*60)

# mne.pick_types() 用于根据通道类型筛选通道索引
# stim=True 表示只选择刺激类型的通道
stim_picks = mne.pick_types(raw.info, stim=True)

print(f"刺激通道索引: {stim_picks}")
# 列表推导式:根据索引获取通道名称
print(f"刺激通道名称: {[raw.ch_names[i] for i in stim_picks]}")

代码解析:

输出解读:

python 复制代码
刺激通道索引: [306]
刺激通道名称: ['STI 014']

数据中有一个刺激通道,索引为 306,名叫 STI 014

3.2 查看刺激通道的原始数据

python 复制代码
# raw.get_data() 获取指定通道的原始数据
# picks='STI 014' 指定获取名为 'STI 014' 的通道
stim_data = raw.get_data(picks='STI 014')

# np.unique() 找出数组中所有不重复的值
unique_values = np.unique(stim_data)
print(f"刺激通道中的唯一值: {unique_values}")

代码解析:

输出示例:

python 复制代码
刺激通道中的唯一值: [0 1 2 3 4 5 32]

这说明数据中有 7 种不同的值:

(1)0****= 无事件(基线状态)

(2)1, 2, 3, 4, 5, 32****= 不同的事件类型编号

四、提取事件(核心步骤)

4.1 使用 find_events 函数

python 复制代码
print("\n" + "="*60)
print("提取事件")
print("="*60)

# mne.find_events() 从刺激通道中自动检测事件
# stim_channel='STI 014' 指定使用哪个通道作为刺激通道
events = mne.find_events(raw, stim_channel='STI 014')

print(f"✅ 提取到 {len(events)} 个事件")

# .shape 返回数组的形状:(行数, 列数)
print(f"事件数组形状: {events.shape}")
print(f"  行数 = 事件总数: {events.shape[0]}")
print(f"  列数 = 3 (采样点, 持续时间, 事件编号)")

代码解析:

find_events() 的工作原理:

(1)监控刺激通道的值

(2)当值从 0 变为非 0 时,记录为事件开始

(3)当值变回 0 时,记录为事件结束

(4)输出事件数组:[开始采样点, 持续采样点数, 事件编号]

4.2 事件数组详解

python 复制代码
# events 是一个 N×3 的二维数组
# 每一行 = [采样点编号, 持续采样点数, 事件编号]
# N = 事件总数

# 查看前 5 个事件
print("\n前 5 个事件:")
print(f"{'采样点':<10}{'持续时间':<10}{'事件编号':<10}{'时间(秒)':<10}")
print("-" * 40)

for i in range(5):
    sample = events[i, 0]       # events[i, 0] = 第 i 个事件的采样点位置
    duration = events[i, 1]     # events[i, 1] = 第 i 个事件的持续时间
    event_id = events[i, 2]     # events[i, 2] = 第 i 个事件的事件编号
    time_sec = sample / raw.info['sfreq']  # 采样点 / 采样率 = 时间(秒)
    print(f"{sample:<10}{duration:<10}{event_id:<10}{time_sec:<10.3f}")

代码解析:

4.3 🔑 关键步骤:发现所有事件编号

这是今天最重要的经验!

python 复制代码
print("\n" + "="*60)
print("🔍 发现所有事件编号(重要!)")
print("="*60)

# np.unique(events[:, 2]) 提取事件数组第2列(事件编号),找出所有唯一值
all_event_ids = np.unique(events[:, 2])
print(f"数据中的所有事件编号: {all_event_ids}")

# 统计每个编号出现的次数
print("\n各事件编号出现次数:")
for eid in all_event_ids:
    # np.sum(events[:, 2] == eid) 统计等于 eid 的行数
    count = np.sum(events[:, 2] == eid)
    print(f"  编号 {eid}: {count} 次")

代码解析:

输出示例:

python 复制代码
数据中的所有事件编号: [1 2 3 4 5 32]

各事件编号出现次数:
  编号 1: 72 次
  编号 2: 73 次
  编号 3: 72 次
  编号 4: 73 次
  编号 5: 12 次
  编号 32: 1 次

五、创建事件 ID 字典

5.1 包含所有事件编号

python 复制代码
print("\n" + "="*60)
print("📝 创建事件 ID 字典")
print("="*60)

# event_id 字典:键 = 有意义的名称,值 = 对应的事件编号
event_id = {
    '听觉/左耳': 1,
    '听觉/右耳': 2,
    '视觉/左眼': 3,
    '视觉/右眼': 4,
    '事件5': 5,      # 补充发现的事件(实际含义需查实验记录)
    '事件32': 32     # 补充发现的事件(实际含义需查实验记录)
}

# 遍历字典,统计每种事件的数量
print("事件 ID 字典:")
for event_name, event_code in event_id.items():
    # .items() 返回 (键, 值) 对
    count = np.sum(events[:, 2] == event_code)
    print(f"  {event_name} (编号{event_code}): {count} 次")

代码解析:

5.2 如果不补全会怎样?

python 复制代码
# ❌ 不完整的事件 ID(缺少编号 5 和 32)
event_id_incomplete = {
    '听觉/左耳': 1,
    '听觉/右耳': 2,
    '视觉/左眼': 3,
    '视觉/右眼': 4
    # 缺少 5 和 32!
}

# 如果使用不完整的 event_id 绘图或分段:
# 会触发 RuntimeWarning 警告
# 编号 5 和 32 的事件会被忽略,可能导致数据丢失

教训:永远先查看数据中实际有哪些事件编号,再创建 event_id!

六、事件可视化

6.1 标准事件分布图

python 复制代码
print("\n" + "="*60)
print("🎨 事件可视化")
print("="*60)

# 使用英文标签(避免 DejaVu Sans 字体不支持中文的问题)
event_id_en = {
    'Auditory/Left': 1,
    'Auditory/Right': 2,
    'Visual/Left': 3,
    'Visual/Right': 4,
    'Event5': 5,
    'Event32': 32
}

print("绘制事件分布图...")

# mne.viz.plot_events() 绘制事件时间线图
fig = mne.viz.plot_events(
    events,                          # 事件数组
    sfreq=raw.info['sfreq'],         # 采样率(用于计算时间轴)
    first_samp=raw.first_samp,       # 数据第一个采样点的编号
    event_id=event_id_en,            # 事件 ID 字典(提供标签)
    show=True                        # 显示图形
)
plt.title('Experimental Events Timeline', fontsize=14)
plt.show(block=True)                 # block=True 保持窗口打开

代码解析:

6.2 自定义事件分布图(支持中文)

python 复制代码
print("\n绘制自定义事件分布图...")

# plt.subplots() 创建画布和坐标轴
# figsize=(12, 4) 设置画布大小:宽12英寸,高4英寸
fig, ax = plt.subplots(figsize=(12, 4))

# 为每种事件选择颜色
colors = ['red', 'blue', 'green', 'orange', 'purple', 'brown']
y_labels = list(event_id.keys())  # 将字典的键转为列表,作为 Y 轴标签

# 遍历每种事件,在图上绘制竖线
for idx, (event_name, event_code) in enumerate(event_id.items()):
    # 创建布尔掩码:该事件类型的行为 True
    mask = events[:, 2] == event_code
    
    # 只处理有出现的事件类型
    if np.sum(mask) > 0:
        # 获取该事件的所有时间点(秒)
        event_times = events[mask, 0] / raw.info['sfreq']
        
        # 创建 Y 轴位置数组(所有竖线在同一水平线上)
        y_pos = np.ones_like(event_times) * idx
        
        # ax.vlines() 绘制竖线
        # y_pos-0.4 到 y_pos+0.4 是竖线的长度
        ax.vlines(event_times, y_pos - 0.4, y_pos + 0.4, 
                 colors=colors[idx % len(colors)], 
                 linewidths=1.5, 
                 label=event_name)

# 设置 Y 轴刻度和标签
ax.set_yticks(range(len(y_labels)))
ax.set_yticklabels(y_labels, fontsize=12)

# 设置 X 轴标签和标题
ax.set_xlabel('时间 (秒)', fontsize=12)
ax.set_title('实验事件分布图', fontsize=14, fontweight='bold')

# 显示图例
ax.legend(loc='upper right', fontsize=8)

# 添加网格线
ax.grid(True, alpha=0.3, axis='x')

# 设置 X 轴范围
ax.set_xlim(0, raw.times[-1])

# 调整布局并保存
plt.tight_layout()
plt.savefig('day3_events_custom.png', dpi=150, bbox_inches='tight')
plt.show(block=True)

6.3 事件类型饼图

python 复制代码
print("\n绘制事件类型饼图...")
fig, ax = plt.subplots(figsize=(8, 6))

# 统计每种事件的数量
event_counts = []
event_labels = []

for event_name, event_code in event_id.items():
    count = np.sum(events[:, 2] == event_code)
    if count > 0:  # 只包含有出现的事件
        event_counts.append(count)
        event_labels.append(f'{event_name}\n(编号{event_code}, n={count})')

# ax.pie() 绘制饼图
ax.pie(
    event_counts,                          # 各部分的数量
    labels=event_labels,                   # 各部分的标签
    autopct='%1.1f%%',                     # 显示百分比,保留1位小数
    colors=plt.cm.Set3(range(len(event_counts))),  # 使用 Set3 配色方案
    textprops={'fontsize': 12}             # 文字属性:字号12
)
ax.set_title('事件类型分布', fontsize=14, fontweight='bold')

plt.tight_layout()
plt.savefig('day3_events_pie.png', dpi=150, bbox_inches='tight')
plt.show(block=True)

代码解析:

七、事件时间统计

python 复制代码
print("\n" + "="*60)
print("⏱️ 事件时间统计")
print("="*60)

for event_name, event_code in event_id_en.items():
    # 筛选该类型的事件
    mask = events[:, 2] == event_code
    
    if np.sum(mask) > 0:
        # 获取该事件所有出现的时间点
        event_times = events[mask, 0] / raw.info['sfreq']
        
        print(f"\n{event_name}:")
        print(f"  出现次数: {len(event_times)}")
        print(f"  首次出现: {event_times[0]:.2f} 秒")
        print(f"  最后出现: {event_times[-1]:.2f} 秒")
        
        # 如果有多个事件,计算间隔
        if len(event_times) > 1:
            # np.diff() 计算相邻元素的差值
            intervals = np.diff(event_times)
            print(f"  平均间隔: {np.mean(intervals):.2f} 秒")
            print(f"  最小间隔: {np.min(intervals):.2f} 秒")
            print(f"  最大间隔: {np.max(intervals):.2f} 秒")

代码解析:

八、数据整合:EEG + 事件

将第2天和第3天的知识结合起来:

python 复制代码
print("\n" + "="*60)
print("🔄 数据整合:EEG + 事件")
print("="*60)

# ===== 使用第2天的方法处理 EEG 数据 =====

# 步骤1:提取 EEG 通道
raw_eeg = raw.copy().pick_types(eeg=True)

# 步骤2:创建标准 Montage
montage = mne.channels.make_standard_montage('standard_1020')

# 步骤3:重命名通道(EEG 001 → Fz, EEG 002 → Cz, ...)
eeg_names = raw_eeg.ch_names
standard_names = montage.ch_names[:len(eeg_names)]

# dict(zip(A, B)) 将两个列表配对成字典
raw_eeg.rename_channels(dict(zip(eeg_names, standard_names)))

# 步骤4:设置电极位置
raw_eeg.set_montage(montage)

print(f"EEG 数据: {len(raw_eeg.ch_names)} 个通道")
print(f"事件数据: {len(events)} 个事件")

# ===== 验证数据一致性 =====
data_start = raw_eeg.times[0]   # 数据开始时间
data_end = raw_eeg.times[-1]    # 数据结束时间
event_times = events[:, 0] / raw.info['sfreq']  # 所有事件的时间

print(f"数据时间范围: {data_start:.1f} - {data_end:.1f} 秒")
print(f"事件时间范围: {event_times[0]:.1f} - {event_times[-1]:.1f} 秒")

# 确认所有事件都在数据范围内
all_in_range = np.all((event_times >= data_start) & (event_times <= data_end))
print(f"所有事件都在数据范围内: {all_in_range}")

print("\n✅ EEG数据 + 事件数据 准备就绪!")
print("可以进入第4天的分段分析")

代码解析:

九、第3天完整可运行代码

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', 'Arial Unicode MS']
plt.rcParams['axes.unicode_minus'] = False

print("="*60)
print("MNE-Python 第3天:事件与标记处理")
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查看刺激通道...")
stim_picks = mne.pick_types(raw.info, stim=True)
print(f"  刺激通道: {[raw.ch_names[i] for i in stim_picks]}")

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

# ---------- 4. 查看数据中有哪些事件编号 ----------
print("\n数据中的所有事件编号:")
all_event_ids = np.unique(events[:, 2])
print(f"  {all_event_ids}")

# 统计每个编号的数量
print("\n各事件编号出现次数:")
for eid in all_event_ids:
    count = np.sum(events[:, 2] == eid)
    print(f"  编号 {eid}: {count} 次")

# ---------- 5. 创建完整的事件 ID 字典 ----------
# 包含所有出现的事件编号
event_id = {
    '听觉/左耳': 1,
    '听觉/右耳': 2,
    '视觉/左眼': 3,
    '视觉/右眼': 4,
    '事件5': 5,      # 补充缺失的事件
    '事件32': 32     # 补充缺失的事件
}

print("\n事件 ID 字典:")
for event_name, event_code in event_id.items():
    count = np.sum(events[:, 2] == event_code)
    print(f"  {event_name} (编号{event_code}): {count} 次")

# ---------- 6. 事件可视化 ----------
print("\n绘制事件分布图...")

# 方法1:使用英文命名避免字体问题(推荐)
event_id_en = {
    'Auditory/Left': 1,
    'Auditory/Right': 2,
    'Visual/Left': 3,
    'Visual/Right': 4,
    'Event5': 5,
    'Event32': 32
}

fig = mne.viz.plot_events(
    events,
    sfreq=raw.info['sfreq'],
    first_samp=raw.first_samp,
    event_id=event_id_en,  # 使用英文标签
    show=True
)
plt.title('Experimental Events Timeline', fontsize=14)
plt.show(block=True)

# ---------- 7. 事件时间统计 ----------
print("\n事件时间统计:")
for event_name, event_code in event_id_en.items():
    mask = events[:, 2] == event_code
    if np.sum(mask) > 0:
        event_times = events[mask, 0] / raw.info['sfreq']
        print(f"  {event_name}:")
        print(f"    次数: {len(event_times)}")
        print(f"    首次: {event_times[0]:.2f}秒")
        print(f"    末次: {event_times[-1]:.2f}秒")

# ---------- 8. 自定义事件图(支持中文) ----------
print("\n绘制自定义事件分布图...")
fig, ax = plt.subplots(figsize=(12, 4))

# 手动绘制事件线
colors = ['red', 'blue', 'green', 'orange', 'purple', 'brown']
y_labels = list(event_id.keys())  # 中文标签

for idx, (event_name, event_code) in enumerate(event_id.items()):
    mask = events[:, 2] == event_code
    if np.sum(mask) > 0:
        event_times = events[mask, 0] / raw.info['sfreq']
        y_pos = np.ones_like(event_times) * idx
        ax.vlines(event_times, y_pos - 0.4, y_pos + 0.4,
                 colors=colors[idx % len(colors)],
                 linewidths=1.5, label=event_name)

ax.set_yticks(range(len(y_labels)))
ax.set_yticklabels(y_labels, fontsize=12)
ax.set_xlabel('时间 (秒)', fontsize=12)
ax.set_title('实验事件分布图', fontsize=14, fontweight='bold')
ax.legend(loc='upper right', fontsize=8)
ax.grid(True, alpha=0.3, axis='x')
ax.set_xlim(0, raw.times[-1])

plt.tight_layout()
plt.savefig('day3_events_custom.png', dpi=150, bbox_inches='tight')
print("  图表已保存为 day3_events_custom.png")
plt.show(block=True)

# ---------- 9. 事件类型饼图(中文) ----------
print("\n绘制事件类型饼图...")
fig, ax = plt.subplots(figsize=(8, 6))

event_counts = []
event_labels = []
for event_name, event_code in event_id.items():
    count = np.sum(events[:, 2] == event_code)
    if count > 0:
        event_counts.append(count)
        event_labels.append(f'{event_name}\n(编号{event_code}, n={count})')

ax.pie(event_counts, labels=event_labels, autopct='%1.1f%%',
       colors=plt.cm.Set3(range(len(event_counts))),
       textprops={'fontsize': 12})
ax.set_title('事件类型分布', fontsize=14, fontweight='bold')

plt.tight_layout()
plt.savefig('day3_events_pie.png', dpi=150, bbox_inches='tight')
print("  饼图已保存为 day3_events_pie.png")
plt.show(block=True)

# ---------- 10. 整合 EEG 数据 ----------
print("\n整合 EEG 数据...")
raw_eeg = raw.copy().pick_types(eeg=True)
montage = mne.channels.make_standard_montage('standard_1020')
eeg_names = raw_eeg.ch_names
standard_names = montage.ch_names[:len(eeg_names)]
raw_eeg.rename_channels(dict(zip(eeg_names, standard_names)))
raw_eeg.set_montage(montage)
print("  EEG数据 + 事件数据 准备就绪")

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

十、今日总结

📝 核心概念

🛠️ 掌握的技能

🔑 核心经验

"先探索,后定义"

不要假设数据中只有 1、2、3、4 号事件。

先用 np.unique(events[:, 2]) 查看实际有哪些编号,再创建 event_id

🔑 关键函数速查

python 复制代码
# 事件提取
events = mne.find_events(raw, stim_channel='STI 014')

# 事件编号探索
all_ids = np.unique(events[:, 2])         # 所有编号
count = np.sum(events[:, 2] == id)         # 某个编号出现次数
times = events[events[:, 2]==id, 0] / sfreq  # 某个编号的时间点

# 事件可视化
mne.viz.plot_events(events, sfreq=..., event_id=...)

# 时间转换
seconds = sample_number / raw.info['sfreq']  # 采样点 → 秒
相关推荐
隔壁大炮12 小时前
MNE-Python 第5天学习笔记:数据预处理(二)—— 伪迹处理
python·eeg·mne·脑电数据处理
夕除12 小时前
spring boot 12
java·开发语言·python
码界筑梦坊12 小时前
141-基于FLask的骑行装备销售订单数据可视化分析系统
python·信息可视化·数据分析·flask·毕业设计·echarts
财经资讯数据_灵砚智能12 小时前
基于全球经济类多源新闻的NLP情感分析与数据可视化(日间)2026年5月23日
大数据·人工智能·python·信息可视化·自然语言处理
晓py12 小时前
博客系统接口测试报告
python
情绪总是阴雨天~12 小时前
Playwright 浏览器自动化完全指南:从入门到实战
python·自动化
fu159357456812 小时前
【使用python代码制作数学逻辑动画】 ——【教程】
开发语言·python
阿拉伯柠檬13 小时前
大语言模型 LLM
人工智能·python·语言模型·自然语言处理·langchain
scan72413 小时前
大模型默认没有记忆
python