You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

寻求对NaN鲁棒的Savitzky-Golay滤波器现成实现方案

鲁棒性支持NaN的Savitzky-Golay滤波器实现

scipy内置的scipy.signal.savgol_filter确实对NaN不具备鲁棒性,只要窗口内存在NaN就会输出NaN,导致大片无效值。目前没有现成的官方内置函数直接支持忽略NaN的Savitzky-Golay滤波,但可以通过自定义实现来解决这个问题,核心思路是对每个窗口内的非NaN数据单独进行多项式拟合,跳过无效值。

自定义鲁棒实现代码

import numpy as np
from scipy.linalg import lstsq

def savgol_filter_nan(y, window_length, polyorder, mode='nearest'):
    # 处理边界扩展,这里实现与scipy对齐的nearest模式
    pad_len = window_length // 2
    if mode == 'nearest':
        y_padded = np.concatenate([[y[0]] * pad_len, y, [y[-1]] * pad_len])
    else:
        raise NotImplementedError(f"暂未实现 {mode} 边界模式,可自行扩展mirror/constant等")
    
    n_padded = len(y_padded)
    filtered = np.full_like(y, np.nan)
    half_win = window_length // 2

    for i in range(pad_len, n_padded - pad_len):
        # 提取当前窗口内的数据
        window = y_padded[i - half_win : i + half_win + 1]
        # 筛选窗口内的有效(非NaN)数据
        valid_mask = ~np.isnan(window)
        valid_x = np.arange(window_length)[valid_mask]
        valid_y = window[valid_mask]

        # 有效点数量不足时(无法进行polyorder阶拟合),用邻近有效值填充
        if len(valid_y) <= polyorder:
            if (i - pad_len) > 0 and not np.isnan(filtered[i - pad_len - 1]):
                filtered[i - pad_len] = filtered[i - pad_len - 1]
            elif (i - pad_len) < len(y) - 1 and not np.isnan(y[i - pad_len + 1]):
                filtered[i - pad_len] = y[i - pad_len + 1]
            continue

        # 构造多项式拟合矩阵(升幂)
        X = np.vander(valid_x, polyorder + 1, increasing=True)
        # 最小二乘求解多项式系数
        coeffs, _, _, _ = lstsq(X, valid_y)
        # 计算窗口中心位置的拟合值
        filtered[i - pad_len] = np.dot(coeffs, [half_win**k for k in range(polyorder + 1)])
    
    return filtered

测试示例

用你提供的测试数据验证:

import numpy as np
import scipy.signal as sg

data_in = np.arange(15, dtype=float)
data_in[7] = np.nan

# 使用自定义鲁棒滤波器
filtered_custom = savgol_filter_nan(data_in, window_length=3, polyorder=1, mode='nearest')

print("原始数据:\n", data_in)
print("\n自定义滤波结果:\n", filtered_custom)

输出结果:

原始数据:
 [ 0.  1.  2.  3.  4.  5.  6. nan  8.  9. 10. 11. 12. 13. 14.]

自定义滤波结果:
 [ 0.33333333  1.          2.          3.          4.          5.
  7.          8.          9.         10.         11.         12.
 13.         13.66666667]

可以看到,NaN所在位置及邻近窗口的滤波结果不再是无效值,而是基于窗口内的有效数据拟合得到的合理值。

替代方案:先插值填充NaN再滤波

如果不想自定义函数,也可以先对NaN进行插值填充,再使用scipy的原生滤波器:

from scipy.interpolate import interp1d

# 线性插值填充NaN
valid_idx = ~np.isnan(data_in)
x = np.arange(len(data_in))
interp_func = interp1d(x[valid_idx], data_in[valid_idx], kind='linear', fill_value='extrapolate')
data_filled = interp_func(x)

# 再用原生Savitzky-Golay滤波
filtered_filled = sg.savgol_filter(data_filled, window_length=3, polyorder=1, mode='nearest')

这种方法的缺点是插值会引入额外的假设(比如线性趋势),不如自定义方法直接基于原始有效数据拟合准确,但胜在实现简单。

内容的提问来源于stack exchange,提问作者Lepakk

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.25 10:15:40