4、数据预处理技术:信号滤波(低通、高通、带通)、去噪、异常值检测与处理
各位同学,欢迎来到数据预处理这一章。
说实话,呼吸机采集到的原始信号,就像菜市场里的录音——什么声音都有。病人的呼吸波形、心跳干扰、管路抖动、甚至隔壁床按铃的声音都可能混进去。如果你直接拿这些数据做分析,那结果基本没法看。
我个人习惯把数据预处理比作「洗菜」。菜不洗干净,炒出来的菜再好看也没人敢吃。信号滤波、去噪、异常值处理,就是呼吸机数据的「清洗三件套」。今天咱们一个一个来拆解。
4.1 信号滤波:把有用的留下,没用的扔掉
滤波的本质很简单:让特定频率的信号通过,把其他频率的拦在外面。呼吸机信号里,我们关心的主要是呼吸频率范围——成人一般是 8-20 次/分钟,换算成频率大约是 0.13-0.33 Hz。但实际采集到的信号里,有 50 Hz 的工频干扰(电网带来的),有高频的肌肉震颤,还有低频的基线漂移。
所以我们需要三种滤波器:
- 低通滤波器:让低频通过,滤掉高频噪声。比如去掉 50 Hz 的工频干扰。
- 高通滤波器:让高频通过,滤掉低频漂移。比如去掉呼吸波形的基线缓慢变化。
- 带通滤波器:只让某个频率范围的信号通过。比如只保留 0.1-0.5 Hz 的呼吸信号。
关键点:滤波器的截止频率不是随便选的。选高了,噪声滤不干净;选低了,有用的呼吸波形会被削平。我一般会先看一眼信号的频谱图,再决定截止频率。
下面是一个 Python 实现的低通滤波器示例,用的是 Butterworth 滤波器——这玩意儿在呼吸机领域很常见,因为它通带平坦,不会把波形搞变形。
import numpy as np
from scipy.signal import butter, filtfilt
def lowpass_filter(data, fs, cutoff=5.0, order=4):
"""
低通滤波器
:param data: 输入信号
:param fs: 采样率 (Hz)
:param cutoff: 截止频率 (Hz)
:param order: 滤波器阶数
"""
nyquist = 0.5 * fs
normal_cutoff = cutoff / nyquist
b, a = butter(order, normal_cutoff, btype='low', analog=False)
# 用 filtfilt 做零相位滤波,避免波形偏移
filtered = filtfilt(b, a, data)
return filtered
# 使用示例
fs = 100 # 假设采样率 100 Hz
raw_signal = np.random.randn(1000) # 模拟原始信号
clean_signal = lowpass_filter(raw_signal, fs, cutoff=5.0)
我的经验:这里我用了 filtfilt 而不是 lfilter。为什么?因为 filtfilt 是零相位滤波,不会让波形产生延迟。在呼吸机这种对实时性要求高的场景里,波形偏移会直接导致报警延迟,这可是要命的事。
4.2 去噪:不只是滤波那么简单
滤波能解决大部分噪声问题,但有些噪声是「非平稳」的——比如病人突然咳嗽一下,或者管路被压了一下。这种噪声在频域上和呼吸信号重叠,用普通滤波器根本分不开。
这时候就需要更高级的去噪方法。我个人比较常用的是:
- 小波去噪:把信号分解成不同尺度,把噪声对应的小波系数置零,再重构信号。对突发性噪声效果很好。
- 中值滤波:对每个点取邻域内的中位数。对付「椒盐噪声」(比如传感器偶尔跳变)特别管用。
- 移动平均:简单粗暴,但会平滑掉波形的尖峰。我一般只在做趋势分析时用。
举个例子,小波去噪的代码长这样:
import pywt
def wavelet_denoise(data, wavelet='db4', level=4):
"""
小波去噪
:param data: 输入信号
:param wavelet: 小波基
:param level: 分解层数
"""
# 小波分解
coeffs = pywt.wavedec(data, wavelet, level=level)
# 估计噪声标准差
sigma = np.median(np.abs(coeffs[-1])) / 0.6745
# 软阈值去噪
threshold = sigma * np.sqrt(2 * np.log(len(data)))
coeffs_thresh = [coeffs[0]] # 近似系数保留
for i in range(1, len(coeffs)):
coeffs_thresh.append(pywt.threshold(coeffs[i], threshold, mode='soft'))
# 重构信号
denoised = pywt.waverec(coeffs_thresh, wavelet)
return denoised
注意:小波去噪的参数很敏感。小波基选错了,或者分解层数太多,会把呼吸波形里的有用细节也滤掉。我曾经在一个项目里试了十几种小波基,最后发现 'db4' 和 'sym8' 对呼吸波形最友好。你可以在自己的数据上多试试。
4.3 异常值检测与处理:别让一个坏点毁了整个分析
呼吸机数据里,异常值太常见了。传感器接触不良、病人突然动了一下、管路积水……都会产生离谱的数据点。如果不处理,这些异常值会严重影响后续的呼吸频率计算、潮气量估算,甚至触发假报警。
异常值检测的方法有很多,我挑几个最实用的:
| 方法 | 原理 | 适用场景 | 缺点 |
|---|---|---|---|
| 3σ 法则 | 超出均值±3倍标准差的值视为异常 | 数据近似正态分布 | 对非正态分布效果差 |
| IQR 方法 | 超出 Q1-1.5*IQR 或 Q3+1.5*IQR 的值视为异常 | 数据分布未知时 | 对尾部数据不敏感 |
| 移动窗口法 | 用滑动窗口内的局部统计量判断 | 信号有趋势变化时 | 窗口大小需要调参 |
| Z-score 改进版 | 用中位数和 MAD 代替均值和标准差 | 数据有较多异常值时 | 计算量稍大 |
我个人最常用的是移动窗口法。为什么?因为呼吸机信号是非平稳的——病人呼吸深度会变,基线也会漂移。用全局的 3σ 法则,会把正常的变化也当成异常。而移动窗口法只跟局部比较,适应性好得多。
下面是一个移动窗口异常值检测的实现:
def moving_window_outlier_detection(data, window_size=50, threshold=3.0):
"""
移动窗口异常值检测
:param data: 输入信号
:param window_size: 窗口大小
:param threshold: 阈值倍数
"""
outliers = np.zeros_like(data, dtype=bool)
for i in range(len(data)):
# 取当前点前后的窗口数据
start = max(0, i - window_size // 2)
end = min(len(data), i + window_size // 2)
window = data[start:end]
# 计算窗口内的均值和标准差
mean = np.mean(window)
std = np.std(window)
# 判断当前点是否异常
if abs(data[i] - mean) > threshold * std:
outliers[i] = True
return outliers
# 处理异常值:用窗口内的中位数替换
def replace_outliers(data, outliers, window_size=50):
cleaned = data.copy()
for i in np.where(outliers)[0]:
start = max(0, i - window_size // 2)
end = min(len(data), i + window_size // 2)
cleaned[i] = np.median(data[start:end])
return cleaned
避坑指南:我曾经在一个项目里直接用 3σ 法则检测异常值,结果把病人用力吸气时的正常高峰值全给滤掉了。后来改成移动窗口法,阈值设成 3.5,效果就好多了。记住:阈值不是死的,要根据你的信号特点来调。我一般会先可视化看一下异常值分布,再决定阈值。
4.4 预处理流程:把三件套串起来
好了,现在我们有滤波、去噪、异常值检测三件套了。但实际项目中,这三步不是随便做的。顺序很重要。
我的标准流程是:
- 先做异常值检测与替换——因为异常值会干扰滤波器的计算(尤其是 IIR 滤波器,一个异常点能震半天)。
- 再做滤波——去掉已知频率范围的噪声。
- 最后做去噪——处理剩下的非平稳噪声。
为什么去噪放最后?因为滤波可能会改变噪声的统计特性,先滤波再去噪,小波阈值会更准确。
下面是一个完整的预处理函数:
def preprocess_respiratory_signal(data, fs):
"""
呼吸机信号预处理完整流程
"""
# 1. 异常值检测与替换
outliers = moving_window_outlier_detection(data, window_size=50, threshold=3.5)
data_clean = replace_outliers(data, outliers, window_size=50)
# 2. 带通滤波:保留 0.1-0.5 Hz 的呼吸信号
from scipy.signal import butter, filtfilt
nyquist = 0.5 * fs
low = 0.1 / nyquist
high = 0.5 / nyquist
b, a = butter(4, [low, high], btype='band')
data_filtered = filtfilt(b, a, data_clean)
# 3. 小波去噪
data_denoised = wavelet_denoise(data_filtered, wavelet='db4', level=4)
return data_denoised
总结一下:数据预处理不是「有就行」,而是要「刚刚好」。滤波太狠,波形失真;去噪太猛,细节丢失;异常值处理太激进,正常数据被误删。你想想看,呼吸机是给人用的,一个错误的预处理可能导致误报警或者漏报警,后果很严重。
所以我的建议是:每做一步预处理,都可视化看一下结果。眼睛是最好的调试工具。
下一章我们会讲特征提取,到时候你会发现——预处理做得好,特征提取事半功倍。预处理做得烂,后面再怎么折腾也是白搭。