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())
我的小技巧:拿到CSV文件后,先用文本编辑器打开看一眼。就一眼,你就能知道它有没有表头、用什么分隔符、有没有空行。这一步能省下你半小时的调试时间。

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
})
注意:SPC文件可能有多个子谱(subspectra)。我曾经遇到过一份文件里存了3条光谱,分别是原始谱、基线校正后的谱、还有差谱。读取时记得检查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 知识体系总览

下面这张图,是我自己总结的光谱数据读取与预处理的全流程。你跟着这个流程走,基本不会出大问题。

光谱数据读取与预处理流程 数据读取 CSV / SPC / OPUS 格式转换 统一为DataFrame 数据清洗 缺失值 / 异常值 预处理 基线校正 / 归一化 质量控制 信噪比 / 重复性检查 输出 干净可用的光谱数据 每个环节都建议做可视化检查,别光看数字 💡 核心原则:先看数据长什么样,再决定怎么处理

3.7 实战小贴士

最后,分享几个我这些年攒下来的经验:

  • 先可视化,再处理:每次读取数据后,第一件事就是画个图看看。肉眼能发现很多算法发现不了的问题。
  • 保留原始数据:我习惯在读入数据后,立刻做一个df_raw = df.copy()。这样万一处理错了,还能回退。
  • 批量处理时注意文件名:我曾经因为文件名编码问题,读错了100多个文件。现在我都用glob.glob()配合os.path来处理路径。
  • 记录处理步骤:每次做了什么处理,参数是什么,都记下来。不然过两周你自己都忘了当时怎么处理的。

嗯,数据读取和预处理这部分,说白了就是「磨刀不误砍柴工」。我见过太多人急着跑模型,结果数据里全是坑,最后分析结果一塌糊涂。花点时间把数据收拾干净,后面的分析会顺畅很多。


公众号:蓝海资料掘金营,微信deep3321