3. 光谱数据读取:常见格式解析与预处理
做光谱分析这么多年,我最大的体会就是:数据读取这步要是没做好,后面再漂亮的算法都是白搭。今天咱们就来聊聊,怎么把那些乱七八糟的光谱文件,变成我们手里干净、可用的数据。
3.1 常见光谱文件格式
先说说市面上最常见的三种格式。我刚开始接触这行的时候,也被这些格式搞得头大。
| 格式 | 扩展名 | 特点 | 常见领域 |
|---|---|---|---|
| CSV | .csv | 纯文本,通用性强 | 几乎所有领域 |
| SPC | .spc | 二进制,包含元数据 | 红外光谱、拉曼光谱 |
| OPUS | .0, .1, .2 | 布鲁克专用格式 | 傅里叶红外光谱 |
CSV格式:说白了就是逗号分隔的文本文件。我最喜欢它,因为用记事本都能打开看。但要注意,有些CSV文件第一行是波长,第二行才是吸光度,这个坑我踩过。
SPC格式:这是Thermo Galactic公司搞出来的二进制格式。它不光存了光谱数据,还存了仪器参数、采集时间这些元数据。我记得有一次,客户给了一堆.spc文件,我直接用文本编辑器打开,全是乱码——嗯,二进制文件嘛,得用专门的库来读。
OPUS格式:布鲁克公司的亲儿子。这格式有点特殊,它把数据分成了多个块(blocks),每个块存不同的信息。你想想看,一个文件里可能同时存了干涉图、光谱图、还有仪器状态——是不是挺复杂的?
3.2 使用pandas读取CSV
pandas是我处理光谱数据的首选工具。为什么?因为它快,而且API设计得很人性化。
import pandas as pd
import numpy as np
# 最简单的读取方式
df = pd.read_csv('spectrum.csv')
# 但实际工作中,我通常会这样写
df = pd.read_csv(
'spectrum.csv',
header=None, # 没有表头
skiprows=2, # 跳过前两行注释
names=['wavelength', 'absorbance'], # 自己命名列
delimiter=',' # 分隔符,有时候是制表符\t
)
# 查看数据概览
print(df.head())
print(df.info())
3.3 读取SPC格式
SPC格式需要用专门的库。我个人推荐pySPC这个库,虽然它更新不太勤快,但够用。
# 安装:pip install pySPC
from pyspc import spc
# 读取SPC文件
file_path = 'sample.spc'
data = spc.File(file_path)
# 获取光谱数据
x = data.x # 波数/波长
y = data.y # 吸光度/透过率
# 查看元数据
print(f"采集时间: {data.date}")
print(f"仪器型号: data.info.get('instrument', '未知')")
# 转换成DataFrame方便后续处理
df_spc = pd.DataFrame({
'wavenumber': x,
'intensity': y
})
data.subspectra这个属性。
3.4 读取OPUS格式
OPUS格式最麻烦。布鲁克官方提供了opus_reader库,但说实话,用起来有点别扭。
# 安装:pip install opus_reader
from opus_reader import opus_reader
# 读取OPUS文件
file_path = 'sample.0'
data = opus_reader.read_opus_file(file_path)
# 提取光谱数据
# 注意:OPUS文件可能包含多个光谱类型
for key in data.keys():
print(f"数据类型: {key}")
if 'AB' in key: # AB表示吸光度谱
wavenumbers = data[key]['x']
absorbance = data[key]['y']
break
# 构建DataFrame
df_opus = pd.DataFrame({
'wavenumber': wavenumbers,
'absorbance': absorbance
})
避坑指南:我曾经花了一整天调试OPUS读取代码,最后发现是文件版本问题。OPUS 7.0和7.5的文件结构略有不同。建议读取前先检查文件头部的版本号。
3.5 数据清洗与预处理
数据读进来了,但别急着分析。光谱数据里藏着不少「脏东西」,得先清理干净。
3.5.1 缺失值处理
光谱数据一般不会有太多缺失值,但偶尔会出现NaN。我通常的做法是:
# 检查缺失值
print(df.isnull().sum())
# 方法1:删除含缺失值的行
df_clean = df.dropna()
# 方法2:插值填充(推荐用于光谱数据)
df_clean = df.interpolate(method='linear')
# 方法3:用前后均值填充
df_clean = df.fillna(method='ffill').fillna(method='bfill')
3.5.2 异常值检测
你想想看,光谱数据里突然出现一个尖峰,是真实的信号还是噪声?我一般用Z-score方法:
from scipy import stats
# 计算Z-score
z_scores = np.abs(stats.zscore(df['absorbance']))
# 设置阈值,通常取3
threshold = 3
outliers = np.where(z_scores > threshold)[0]
print(f"发现 {len(outliers)} 个异常点")
# 将异常点替换为插值
df_clean = df.copy()
df_clean.loc[outliers, 'absorbance'] = np.nan
df_clean['absorbance'] = df_clean['absorbance'].interpolate()
3.5.3 基线校正
这是光谱预处理里最关键的步骤。说白了,基线漂移会让你的定量分析完全不准。
# 简单的基线校正:减去最小值
df['corrected'] = df['absorbance'] - df['absorbance'].min()
# 更专业的:使用ALS算法(非对称最小二乘)
from scipy import sparse
from scipy.sparse.linalg import spsolve
def baseline_als(y, lam=1e5, p=0.01, niter=10):
L = len(y)
D = sparse.diags([1, -2, 1], [0, -1, -2], shape=(L, L-2))
w = np.ones(L)
for i 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 z
baseline = baseline_als(df['absorbance'].values)
df['baseline_corrected'] = df['absorbance'] - baseline
3.5.4 归一化处理
不同批次的光谱,强度可能差很多。归一化能让它们站在同一起跑线上。
# 最大最小值归一化
df['normalized'] = (df['absorbance'] - df['absorbance'].min()) / \
(df['absorbance'].max() - df['absorbance'].min())
# 面积归一化(常用)
area = np.trapz(df['absorbance'], df['wavelength'])
df['area_normalized'] = df['absorbance'] / area
# 向量归一化(SNV)
mean_val = df['absorbance'].mean()
std_val = df['absorbance'].std()
df['snv_normalized'] = (df['absorbance'] - mean_val) / std_val
3.6 知识体系总览
下面这张图,是我自己总结的光谱数据读取与预处理的全流程。你跟着这个流程走,基本不会出大问题。
3.7 实战小贴士
最后,分享几个我这些年攒下来的经验:
- 先可视化,再处理:每次读取数据后,第一件事就是画个图看看。肉眼能发现很多算法发现不了的问题。
- 保留原始数据:我习惯在读入数据后,立刻做一个
df_raw = df.copy()。这样万一处理错了,还能回退。 - 批量处理时注意文件名:我曾经因为文件名编码问题,读错了100多个文件。现在我都用
glob.glob()配合os.path来处理路径。 - 记录处理步骤:每次做了什么处理,参数是什么,都记下来。不然过两周你自己都忘了当时怎么处理的。
嗯,数据读取和预处理这部分,说白了就是「磨刀不误砍柴工」。我见过太多人急着跑模型,结果数据里全是坑,最后分析结果一塌糊涂。花点时间把数据收拾干净,后面的分析会顺畅很多。
公众号:蓝海资料掘金营,微信deep3321