4. 数据完整性检查:缺失值检测、扫描一致性

数据完整性,说白了就是看你的质谱数据有没有「缺胳膊少腿」。我之前处理过一批临床样本的数据,刚开始跑得挺顺,结果后面发现某个时间段的谱图全是空的。一查,是采集软件中途崩了,但日志里没报错。这种事其实不少见,所以完整性检查必须放在质控流程的第一步。

核心观点:数据完整性检查就像在盖楼前检查地基,地基不牢,后面再牛的算法都白搭。缺失值和扫描不一致,是地基上最常见的两条裂缝。

4.1 缺失值检测

缺失值,就是本该有数据的地方,它没来。在质谱数据里,缺失值可能出现在三个层面:

  • 扫描级缺失:某个扫描周期的信号全为空或接近零值。
  • 特征级缺失:某个保留时间-质荷比组合下,没有检测到峰。
  • 样本级缺失:整张谱图都丢了,或者文件损坏无法读取。

我个人的习惯是先做扫描级和样本级的检查,因为这两层最容易定位根因。

4.1.1 扫描统计指标

常用的几个指标,直接看表格:

指标 定义 阈值参考
空扫描占比 总强度低于噪音阈值的扫描数 / 总扫描数 < 5%
扫描强度变异系数 各扫描总强度的变异系数 < 30%
缺失特征比例 缺失的特征数 / 总特征数 < 20%
连续缺失窗口 连续缺失的最大扫描数 < 5 个扫描

你可能会问:这些阈值怎么来的?嗯,这里要注意,没有万能阈值。我一般会根据仪器类型和样本基质来调。比如对于高分辨质谱,空扫描占比超过2%我就开始警觉了;但对于低分辨率的在线监测数据,5%以内也算正常。

4.1.2 常用检测方法

分三种:统计扫描法、可视化检查法、基于规则的检测法。

统计扫描法

最直接的方式,就是计算每个扫描的TIC(总离子流)强度分布。如果某个扫描的TIC低于全局TIC中位数的10%,我基本会把它标为「空扫描」。代码里可以这样做:

import numpy as np

def detect_missing_scans(tic_array, noise_fraction=0.1):
    tic_median = np.median(tic_array)
    threshold = tic_median * noise_fraction
    missing = np.where(tic_array < threshold)[0]
    return missing

missing_scans = detect_missing_scans(tic_data)
print(f"发现 {len(missing_scans)} 个空扫描")

我在项目中遇到过,某次用这个方法检测时,发现一批数据里有连续30个空扫描。当时还以为是仪器故障,结果后来发现是进样针堵了。你看,这种就是通过统计扫描法很快能锁定的问题。

可视化检查法

有时候数字不直观,我会做个热图或者折线图。把扫描序号当x轴,TIC强度当y轴,一眼就能看出哪里断流了。不过,我一般不依赖纯人工检查,它会和统计扫描法配合用。

基于规则的检测法

这个方法适合处理已知规律的数据。比如你的方法是运行5分钟,前1分钟是溶剂前沿,中间3分钟出峰,最后1分钟是冲洗——那就可以定规则:前1分钟的空扫描可以接受,但如果中间3分钟有超过2个连续空扫描,就要报警。

def rule_based_detection(tic_array, scan_times, method_config):
    # method_config: {start_min, end_min, max_gap}
    middle_mask = (scan_times >= method_config['start_min']) & \
                  (scan_times <= method_config['end_min'])
    middle_tic = tic_array[middle_mask]
    # 找连续空扫描序列
    missing = middle_tic < (np.median(tic_array) * 0.1)
    gap_lengths = np.diff(np.where(np.diff(np.concatenate(([False], missing, [False]))))[0])[::2]
    if np.any(gap_lengths > method_config['max_gap']):
        return True  # 不合格
    return False  # 合格

我的经验:规则检测法的「规则」一定要在实验前就定好,边检测边调规则很容易过拟合。我曾经吃过这个亏,调出来的规则只适用于那一批数据,换批样本全废了。

4.2 扫描一致性检查

缺失值查完了,接下来要看扫描的一致性。什么叫一致性?就是你这次采集的10000个扫描,和上次采集的10000个扫描,它们的分布特征是不是高度相似的?说白了,就是看仪器在同样的实验条件下,有没有「心不在焉」。

4.2.1 需要检查哪些方面

我觉得至少要覆盖三个维度:

  1. 保留时间对齐度(Retention Time Alignment)—— 峰的位置有没有偏移。
  2. 质量轴校准稳定性(Mass Axis Stability)—— m/z 漂移在不在容忍范围内。
  3. 峰检测一致性(Peak Consistency)—— 同样的样本,峰数量、峰面积有没有突变。

你可以把这三个指标放进一个表格里,定期打勾,像做体检一样:

检查项 度量方式 合格标准
保留时间偏移 内标峰RT的变异系数 CV < 1%
质量轴漂移 已知离子m/z偏差的绝对值 < 5 ppm
峰数量变异 连续3针的峰数量差异 < ±10%
总峰面积波动 各扫描TIC的变异系数 CV < 15%

4.2.2 实际操作示例

假设我们有一批LC-MS数据,先加载并计算每针的TIC分布。这个示例展示了如何计算保留时间窗内的一致性:

def check_rt_consistency(peak_list, rt_window=0.1):
    # peak_list: [(rt, intensity), ...]
    for i in range(1, len(peak_list)):
        rt_diff = abs(peak_list[i][0] - peak_list[0][0])
        if rt_diff > rt_window:
            return False, f"RT偏移超过 {rt_window} min"
    return True, "RT一致性通过"

具体到实际项目,我会再加上一个滑动窗口来扫描整个色谱段。因为我遇到过一个情况:前几分钟的保留时间完全对得上,到了后面几分钟偏移越来越大。只用单个点检查,根本抓不到。

避坑提示:做扫描一致性检查时,别只看平均值。我记得有次跳过全程窗口检查,只取中间3分钟做统计,结果全都合格。后来发现前1分钟和后1分钟的峰面积差异巨大,粗看是没问题的,实际上数据已经不能用了。所以,要做全程逐段的切片检查,别偷懒。

4.3 数据完整性检查的整体流程

把缺失值检测和扫描一致性检查串成一条流水线,每次数据采集完就跑一遍。这个流程图我习惯拿来做日常质控的骨架:

数据完整性检查全流程 1. 数据接收 / 文件完整性 2. 缺失值检测 3. 扫描一致性检查 是否全部通过? 通过 → 继续分析 不通过 → 复查/重新采集 说明 缺失&不一致 → 标记为可疑数据 全部通过 → 进入下游定量/定性分析

这张图上画得很清楚:数据进来后先做文件级别的完整性检查,然后扫缺失值,再查扫描一致性,最后二分法决定能不能往下走。

我的小技巧:把流程自动化。每次采集结束后,后台自动跑一遍这个检查链,把结果写到日志里。我早期都是手动跑,但有时候忙起来就忘了。自从写了个批处理脚本老老实实跑之后,再没漏检过。

4.4 小结与注意事项

数据完整性检查是质控的第一道门。我个人总结了几条必记内容:

  • 缺失值检测要区分层面:扫描级、特征级、样本级,不能混在一起看。
  • 扫描一致性检查要覆盖保留时间、质量轴、峰特征三个维度。
  • 阈值不是死的,要根据仪器历史和样本类型动态调整。
  • 流程要自动化,不能完全依赖人工。人工的强项是诊断,不是扫描。

我曾经有一个案例就是:用了一套固定的阈值跑了半年,一直没问题。结果某次换了批血浆样本,缺失值占比飙到12%,但我的阈值是20%,直接通过了。然后下游的定量分析结果出现系统性偏差,查了两天才找到原因——样本基质抑制导致部分扫描强度被大幅压低。自那以后,我每个项目都重设阈值,不再复用之前的。

一句话记住:缺了的数据不会自己回来,不一致的信号只会误导你。数据完整性检查,是你在高分辨质谱海量数据处理前,给自己上的第一道保险。