
简介面向睡眠科研与脑电数据分析人员的 YASA 睡眠分析工具箱资源包定位为可直接安装使用的 Python 源码项目。YASA 支持多导睡眠图自动分期、纺锤波/慢波/快速眼动事件检测、伪影剔除以及频谱、相位幅度耦合与 1/f 斜率等分析适用于熟悉 NumPy、Pandas、MNE 并希望用 Jupyter Lab 开展睡眠数据研究的初学者和进阶用户。资源包共 254 个文件约 115.78MB主体为 27 个 py 源码文件与 16 个 ipynb 示例笔记本另含 59 个 png 图例、42 个 html 文档及 css/js 等网页资源以及若干测试数据与配置文件目录结构完整便于搭建环境后快速复现各项分析流程。内容涵盖从数据导入、事件检测到睡眠分期与统计输出的完整调用示例并附带构建文档和演示图表。已有 803 人浏览学习适合需要处理睡眠脑电数据并构建自动化分析管线的研究者参考。1. 用 YASA 把多导睡眠图变成可二次计算的 Python 数据拿到一份带标注的 PSG 记录人工分期通常要花掉小半天从夜间 10 点到第二天 6 点按 30 秒一页翻脑电、眼电、肌电三个通道轮流看。YASA 做完同一件事只要几十秒而且它最初不是为分期设计的。它的全称 Yet Another Spindle Algorithm本意是自动检测睡眠纺锤波后来扩展成完整的自动睡眠分期流水线现在还能分析慢波、微觉醒和睡眠结构。这个 Python 包适合两类人一类是做睡眠研究的临床科研人员需要按脚本分析几十上百份记录另一类是数据工程师和算法工程师需要把 PSG 里的睡眠阶段变成可计算的标签或特征。关键在于 YASA 的输出不是一张报告图片而是 pandas DataFrame每个事件、每个分期都可以继续加工。2. 多导睡眠图与 YASA 的输入约定先读懂 EDF、通道名与算法边界几乎所有人第一次跑yasa.sleep_staging报错都是因为通道配置不符合约定。PSG 数据以 EDF/EDF 格式居多本质上是多通道时间序列的容器通道名、采样率、物理单位和校准系数都存在文件头里。YASA 不会自动知道哪条是脑电哪条是眼电或者肌电它通过通道名字符串匹配并依赖 MNE 的Raw对象来理解数据。因此在调用任何 YASA 函数之前我会先确认四件事数据能被 MNE 正常读取、采样率符合预期、通道名能对应到 YASA 的默认别名、脑电信号极性是否正常。2.1 多导睡眠图记录里的核心信号标准过夜 PSG 至少包含三路脑电、两路眼电和一路下颌肌电。脑电按 10-20 系统放置常见位置是 Fz/Cz/Oz 或者 F3/C3/O1采样率在 100 Hz 到 500 Hz 之间EOG 记录眼球电位差REM 期会出现快速共轭眼动EMG 记录颏舌肌张力REM 时肌张力会显著下降。再加上心电、腿动、呼吸、血氧饱和度整份记录通常超过 30 个通道但自动睡眠分期并不需要全部通道。YASA 只取三个 EEG 通道、一个 EOG 和一个 EMG剩余通道会在特征计算之外被忽略。2.2 通道名映射YASA 如何找到 Fz、Cz、OzYASA 内部对通道名的关键字匹配是“包含”而不是“等于”。比如通道标注EEG Cz、CZ、Cz-REF都能命中Cz关键字EOG LOC能命中eogChin1能命中emg。如果数据集里只有F3、C3和O1则必须显式告诉 YASA 这三个通道分别扮演什么角色而不是去改 EDF 文件。YASA 参数默认匹配关键字常见可接受别名eeg_fzFzFz,FZ,EEG Fz,F3eeg_czCzCz,CZ,EEG Cz,C3eeg_ozOzOz,OZ,EEG Oz,O1eogEOGEOG,EOGL,EOGR,LOC,ROCemgEMGEMG,Chin,Chin1,EMG1这个表同样适用于spindles_detect和slow_wave_detect检测时如果只传入一段 ndarray函数只需要采样率sf如果传入 MNERaw则建议同时传ch_name告诉 YASA 使用哪条通道。2.3 YASA 自动分期的算法骨架与适用边界sleep_staging的思路不是深度学习而是一套手工特征加随机森林从每 30 秒的数据窗中提取 EEG 各频段相对功率、频谱斜率、EOG 方差、EMG 低幅比例等特征然后由随机森林分类器给出 W/N1/N2/N3/REM 的概率。模型在训练集上对健康成人整夜数据的 Kappa 普遍在 0.7 左右但这个表现依赖通道配置和睡眠结构。如果记录来自儿童、老年期认知障碍患者或者受试者在记录中大量翻动YASA 的误判会明显增多。处理这类问题前先在原始信号上做一次性检查import mne raw mne.io.read_raw_edf(demo.edf, preloadTrue, verboseFalse) print(raw.info[sfreq]) # 采样率 print(raw.get_channel_types()) # 通道类型列表 print(raw.ch_names) # 全部通道名输出结果如果是misc、stim混杂说明 MNE 没有正确识别通道类型。常见做法是用raw.set_channel_types({Fz: eeg, EOG: eog, EMG: emg})手动纠正必要时对非脑电通道做重命名再运行 YASA。这段代码的价值在于尽早暴露 EDF 标签问题YASA 报错或者给出全是 Wake 的结果时90% 是通道类型不对而不是算法能力不足。3. 从 Python 安装到跑通 YASA环境、pip 与第一批睡眠分期YASA 是纯 Python 包依赖 NumPy、SciPy、pandas、scikit-learn 和 MNE不依赖 GPU也不要求深度学习框架。这意味着安装链路比许多现代分析工具短但环境仍值得单独建一个因为 MNE 和 pandas 的版本对 YASA 输出有直接影响。3.1 安装 Python 环境的三种方式与踩坑在 Windows 或 macOS 本地直接安装 Python 3.10 或 3.11然后在 VSCode 里选中同一个解释器即可。很多 Python 安装教程只教你装最新版但 YASA 对最新 Python 的支持往往滞后更稳妥的是装 3.10 或 3.11。在 Linux 服务器上批量分析时用虚拟环境隔离依赖避免污染系统自带 Pythonpython3 -m venv ~/psg_env source ~/psg_env/bin/activate pip install --upgrade pip命令行里不要用sudo pip install。混用系统包管理器与 pip 是环境出问题的主要来源轻则 import 失败重则覆盖系统包。VSCode 里配置 Python 环境时需要在.vscode/settings.json中确保python.defaultInterpreterPath指向虚拟环境。3.2 安装 YASA 并验证依赖在虚拟环境激活后安装pip install yasa python -c import yasa; print(yasa.__version__)第一条命令装 YASA 及其核心依赖第二条命令验证版本号和 import 路径。如果 import 报ModuleNotFoundError优先检查当前解释器是否在虚拟环境内再用pip list | grep -i mne看 MNE 是否装上。导出 Excel 报告时另装openpyxl不装也不影响分期和事件检测。提示不要在 conda 和 pip 混装的情况下使用 sudo pip install权限问题会让 Python 找到错误的包路径。3.3 最小可复现加载 EDF 并运行 sleep_staging下面的代码可以从一份 EDF 文件直接得到整夜分期。通道名按常见 PSG 数据集的命名来写如果你的文件名是 F3/C3/O1把参数换成对应的实际通道名即可。import mne import yasa # 读取 EDFpreloadTrue 让数据进入内存避免后续反复磁盘 IO raw mne.io.read_raw_edf(subject01.edf, preloadTrue, verboseFalse) # 只保留分期需要的通道减小计算量 keep [ch for ch in [Fz, Cz, Oz, EOG, EMG] if ch in raw.ch_names] raw.pick_channels(keep) # 执行自动睡眠分期hypno 是 30 秒一页的分期结果 staging yasa.sleep_staging( raw, eeg_fzFz, eeg_czCz, eeg_ozOz, eogEOG, emgEMG, metadatadict(subject_idsub-01), ) hypno staging.sleep_stages if hasattr(staging, sleep_stages) else staging print(hypno.head(10))这段代码的逻辑是先用 MNE 读取 EDF然后pick_channels把不参与分期的呼吸、腿动、血氧等通道丢弃最后把通道别名传给sleep_staging。eeg_fz、eeg_cz、eeg_oz三个参数指定脑电位置eog和emg指定眼电和肌电。如果原始文件只有 F3/C3/O1直接修改这些参数的值例如eeg_fzF3不需要修改 EDF 内部标注。3.4 看懂 sleep_staging 输出分期的含义与常用参数hypno是长度为整夜页码的数组或 Series每 30 秒一个值。YASA 的输出约定是-1 表示未评分0 表示 Wake1 表示 N12 表示 N23 表示 N34 表示 REM。用下面代码可以快速看整夜分布import pandas as pd hypno pd.Series(hypno) stage_map {-1: Unscored, 0: Wake, 1: N1, 2: N2, 3: N3, 4: REM} print(hypno.map(stage_map).value_counts().sort_index())sleep_staging经常改的参数有四个参数作用设置建议l_freqEEG 高通频率默认 0.5 Hz有严重漂移时可提到 1 Hzh_freqEEG 低通频率默认 45 Hz不要低于 30 Hz会切掉纺锤波downsample是否重采样到 128 Hz原始采样率已经是 128 时设 False 省算力verbose控制日志输出批量跑设为 False如果某个受试者整夜都是 Wake先检查原始波形里是否真的包含睡眠信号再看raw.get_channel_types()是否把 EEG 标成了misc。这个问题在公开数据集里很常见。4. 用 YASA 深入分析 PSG纺锤波、慢波、睡眠结构与批量脚本sleep_staging只是入口。YASA 名字里的 Spindle 指睡眠纺锤波这也意味着事件检测才是它的看家本领。拿到 hypno 之后我一般会立刻跑纺锤波和慢波检测因为这两个事件直接决定睡眠深度也是很多科研项目真正需要的特征。4.1 用 spindles_detect 检测睡眠纺锤波并理解每列输出纺锤波是 N2 期的标志性事件频率集中在 10-16 Hz持续时间 0.3-2 秒。YASA 提供spindles_detect输入可以是 MNERaw对象也可以是裸数组加采样率。推荐传入Raw这样能利用通道名信息sf raw.info[sfreq] result yasa.spindles_detect(raw, sfsf, hypnohypno, freq_range(10, 16), duration(0.3, 2.0)) spindles result[0] if isinstance(result, tuple) else result print(spindles.columns)返回的每一行代表一个检测到的纺锤波。常见列包括Start、Peak、End、Duration、Frequency、Amplitude、RMS、Power和Stage。Stage是 YASA 根据传入的 hypno 标注的事件所在睡眠期可以直接按Stage分组统计spindles.groupby(Stage).agg( event_count(Start, count), avg_duration(Duration, mean), avg_frequency(Frequency, mean) )关键参数表格参数默认值说明hypnoNone不传则全夜检测传入则只在对应睡眠期检测freq_range(10, 16)儿童或老年人可扩展到 (9, 16)duration(0.3, 2.0)太短通常是 alpha 伪迹不是纺锤波relative_power0.2调大减少误检调小提高灵敏度一个常被忽视的坑是幅度单位。MNE 读取 EDF 后如果通道单位是 μVAmplitude单位也近似是 μV。如果原始数据被转成整数型 int16幅度数值会大几千倍检测阈值完全失效。所以批量分析时应先把数据统一到 float32 和 μV 单位。4.2 用 slow_wave_detect 检测慢波并提取慢波密度慢波集中在 N3 期Delta 频率 0.5-4 Hz幅度高是深度睡眠的核心指标。YASA 提供的slow_wave_detect和纺锤波检测类似但参数偏向于负相波的持续时间和幅度sw yasa.slow_wave_detect(raw, sfsf, hypnohypno, dur_neg(0.3, 1.5), amp_neg(40, 180)) sw sw[0] if isinstance(sw, tuple) else sw sw[duration] sw[End] - sw[Start]dur_neg定义负相持续时间amp_neg定义负相峰幅度范围单位同样跟随输入信号单位。如果慢波检出太少把amp_neg下界从 40 降到 30如果检出太多疑似肌电干扰把上界从 180 调低。医学文献里常用慢波密度指标即每小时 N3 睡眠中的慢波数量total_sleep_time yasa.hypno_summary(hypno, sf_hypno1/30).get(TST, 0) sw_density len(sw) / (total_sleep_time / 3600) print(Slow wave density:, round(sw_density, 2), events/hour)慢波检测的通道建议选用 Fz 或 Fz 对应位置因为慢波在前额区域幅度最大。用 Oz 检测会漏掉不少事件。4.3 计算睡眠结构TST、SE、各期比例用 hypno_summary睡眠结构参数是报告里最常出现的表格总睡眠时间、睡眠效率、入睡潜伏期、入睡后醒来时间、各期比例。YASA 提供hypno_summary一行代码直接算完summary yasa.hypno_summary(hypno, sf_hypno1/30) print(summary)sf_hypno表示分期序列的采样率。因为 hypno 每 30 秒一个点所以是1/30。返回结果通常包含TST、SE、SOL、WASO、各期分钟数与百分比。如果需要把每 30 秒的分期标签对齐到每个采样点用yasa.hypno_upsample_to_data配合np.zeros(raw.n_times)作为长度占位import numpy as np hypno_sample yasa.hypno_upsample_to_data( hypno, sf_hypno1/30, datanp.zeros(raw.n_times) )这样做的好处是后续做脑电谱分析时可以直接用布尔索引提取 N2 或 N3 的连续片段而不必反复计算页码边界。4.4 批量跑几十个 EDF 文件时的内存管理与断点记录当文件数量超过 10 个脚本必须能失败后继续跑下去。典型做法是遍历目录每个文件用try-except捕获异常并把已完成的 hypno 存成.npy文件from pathlib import Path import numpy as np out_dir Path(results) out_dir.mkdir(exist_okTrue) for f in Path(edf).glob(*.edf): try: raw mne.io.read_raw_edf(f, preloadTrue, verboseFalse) keep [ch for ch in [Fz, Cz, Oz, EOG, EMG] if ch in raw.ch_names] if len(keep) 5: print(f{f.name}: not enough channels, skip) continue raw.pick_channels(keep) st yasa.sleep_staging(raw, eeg_fzFz, eeg_czCz, eeg_ozOz, eogEOG, emgEMG) hypno st.sleep_stages if hasattr(st, sleep_stages) else st np.save(out_dir / (f.stem _hypno.npy), hypno) except Exception as err: print(f.name, ERROR, err)preloadTrue会让大文件直接载入内存但 YASA 必须随机访问多个时间窗因此这一步不能省。大批量时每跑完一个文件就立刻释放内存不让raw变量跨循环累积若单文件超 1GB可先用raw.pick_channels裁剪通道把内存峰值从几 GB 降到几百 MB。打印日志时不要只用print最后汇总失败列表再针对失败的EDF单独排查。5. 验证 YASA 分期与事件检测的三个技巧5.1 与人工分期比一致性先算 KappaYASA 的自动分期不能盲信。科研文章中要求报告与人工分期的一致性通常用 Cohen’s Kappa。前提是两边都是同一时长、同一 30 秒网格的整数标签。先用astype(int)统一类型再用 scikit-learn 计算from sklearn.metrics import cohen_kappa_score yasa_int yasa_hypno.astype(int) manual_int manual_hypno.astype(int) kappa cohen_kappa_score(yasa_int, manual_int) print(Kappa:, kappa)Kappa 超过 0.6 可以接受0.7 以上被认为一致良好。如果 Kappa 低于 0.5优先比较 N1 与 Wake 的混淆因为 YASA 对 N1 的识别最不稳定。5.2 把检测事件画回原始波形查假阳性事件检测不能只看数量。用 MNE 的 Annotations 把纺锤波起点和持续时间标回原始信号import mne annot mne.Annotations( onsetspindles[Start].values, durationspindles[Duration].values, description[spindle] * len(spindles), ) raw.set_annotations(annot) raw.plot(start100, duration30, scalingsauto)可视化能快速暴露两类问题一是把 alpha 波当成了纺锤波二是把 EMG 突触当成慢波。批量分析前随机抽 3-5 个文件肉眼确认比事后看统计数字更可靠。5.3 固定依赖版本建立回归基线YASA 依赖 MNE、pandas、scikit-learn任何一个版本变化都可能改变分期结果。合理做法是把环境和基线输出都保存下来pip freeze requirements_lock.txt选定一个标准文件运行完整分析把hypno_summary和纺锤波统计保存为 CSV作为基线。后续升级依赖后用pd.testing.assert_frame_equal对比当前结果与基线结果pd.testing.assert_frame_equal(current, baseline, check_exactFalse, rtol1e-3)允许微小浮点误差但TST、SE增幅超过几个百分点时必须查版本变更说明。把yasa.__version__和scikit-learn.__version__写进 README是成本最低的复现保障。本文还有配套的精品资源点击获取