4. 数据清洗与预处理:去除基线漂移、平滑去噪、归一化、扣除背景光谱
拿到原始光谱数据,别急着分析。我见过太多人上来就跑模型,结果被基线漂移坑得找不着北。数据清洗这步,说白了就是给光谱「卸妆」——把那些不属于样品本身的干扰信号统统去掉。
这一章我们重点解决四个问题:基线漂移怎么扣?噪声怎么滤?数据怎么归一?背景怎么减?嗯,一个一个来。
4.1 去除基线漂移
基线漂移是个老顽固。温度变化、光源老化、探测器响应不均,都会让基线慢慢「飘」起来。我早期做近红外分析时,有批数据基线飘了将近20%,差点把模型搞崩。
常用的方法有两种:多项式拟合和ALS算法。
4.1.1 多项式拟合
思路很简单:找一段没有样品吸收的「空白区」,用多项式把基线形状拟合出来,然后从原始光谱里减掉。
我个人习惯用3~5阶多项式。阶数太低拟合不准,太高容易过拟合——把真实信号也当基线扣掉了。
import numpy as np
from numpy.polynomial import polynomial as P
def poly_baseline(x, y, degree=3, baseline_region=None):
"""
多项式拟合基线
x: 波数/波长
y: 光谱强度
degree: 多项式阶数
baseline_region: 基线区域索引,如 [(0,200), (1800,2000)]
"""
if baseline_region is None:
# 默认取两端各10%作为基线区域
n = len(x)
baseline_region = [(0, int(n*0.1)), (int(n*0.9), n-1)]
mask = np.zeros_like(x, dtype=bool)
for start, end in baseline_region:
mask[start:end] = True
coeffs = P.polyfit(x[mask], y[mask], degree)
baseline = P.polyval(x, coeffs)
return y - baseline, baseline
4.1.2 ALS算法(不对称最小二乘法)
ALS算法是近年来的主流。它通过迭代加权,自动把基线「压」在信号下方。说白了,就是让基线尽量贴着光谱的下包络走。
def als_baseline(y, lam=1e5, p=0.01, niter=10):
"""
ALS算法去除基线
y: 光谱强度
lam: 平滑参数(越大越平滑)
p: 不对称权重(越小越严格)
"""
from scipy import sparse
from scipy.sparse.linalg import spsolve
L = len(y)
D = sparse.diags([1, -2, 1], [0, -1, -2], shape=(L, L-2))
w = np.ones(L)
for _ in range(niter):
W = sparse.spdiags(w, 0, L, L)
Z = W + lam * D.dot(D.T)
z = spsolve(Z, w * y)
w = p * (y > z) + (1 - p) * (y < z)
return y - z, z
4.2 平滑去噪
光谱里的噪声,就像照片里的噪点。不处理吧,后续分析全是毛刺;处理过头吧,细节又丢了。这里我推荐两个工具:Savitzky-Golay滤波器和移动平均。
4.2.1 Savitzky-Golay滤波器
SG滤波器是我最常用的。它用局部多项式拟合来平滑数据,既能去噪又能保留峰的形状。窗口大小和多项式阶数是关键。
from scipy.signal import savgol_filter
def sg_smooth(y, window_length=11, polyorder=3):
"""
Savitzky-Golay平滑
window_length: 窗口长度(必须为奇数)
polyorder: 多项式阶数
"""
return savgol_filter(y, window_length, polyorder)
4.2.2 移动平均
移动平均更简单粗暴——取相邻点的平均值。优点是快,缺点是会把峰展宽。适合噪声不大、峰比较宽的情况。
def moving_average(y, window=5):
"""
移动平均平滑
"""
return np.convolve(y, np.ones(window)/window, mode='same')
| 方法 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|
| SG滤波器 | 保留峰形,去噪效果好 | 计算稍慢 | 精细分析、窄峰 |
| 移动平均 | 速度快,实现简单 | 展宽峰,损失细节 | 快速预览、宽峰 |
4.3 归一化
归一化是为了消除样品厚度、浓度、光程等差异带来的强度变化。你想想看,同一瓶溶液,装在不同厚度的比色皿里,光谱强度能一样吗?
常用的有三种:最大最小值归一、面积归一、向量归一。
4.3.1 最大最小值归一
把光谱缩放到[0,1]区间。简单直观,但容易受噪声影响——一个异常点就能把整个范围拉偏。
def minmax_normalize(y):
return (y - y.min()) / (y.max() - y.min())
4.3.2 面积归一
把光谱下的总面积归一化为1。适合比较峰的形状,不受绝对强度影响。我在做混合物分析时常用这个。
def area_normalize(x, y):
area = np.trapz(y, x)
return y / area
4.3.3 向量归一
把光谱看作一个向量,归一化到单位长度。这是化学计量学里的标准做法,尤其适合PCA、PLS等多元分析。
def vector_normalize(y):
norm = np.linalg.norm(y)
return y / norm
4.4 扣除背景光谱
背景光谱,就是没有样品时测到的信号。它包含了光源、光学元件、溶剂等的贡献。扣除背景,说白了就是「空白的信号不要算在样品头上」。
标准做法是:先测背景,再测样品,然后直接相减。
def subtract_background(sample, background):
"""
扣除背景光谱
sample: 样品光谱
background: 背景光谱
"""
return sample - background
但现实往往没那么理想。背景和样品可能不是同时测的,或者环境发生了变化。这时候就需要一些技巧:
- 动态背景扣除:如果背景随时间漂移,可以用插值法在两次背景测量之间估算当前背景。
- 多点背景扣除:测多个背景点取平均,减少随机误差。
- 加权背景扣除:根据样品和背景的测量时间差,给不同背景点分配权重。
知识体系总览
下面这张图把本章的核心逻辑串起来了。你可以把它当作操作流程图:拿到原始光谱后,先扣基线,再平滑去噪,然后归一化,最后扣除背景。每一步都有多种方法可选,具体用哪个取决于你的数据和目标。
数据清洗没有银弹。每种方法都有它的脾气,关键是多试、多看、多对比。我做了十年光谱分析,到现在拿到新数据还是会先画几张图看看效果。别嫌麻烦——数据清洗做得好,后续分析事半功倍。
公众号:蓝海资料掘金营,微信deep3321