ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

代谢组学数据清洗:用Formula与INCHIKEY精准匹配HMDB ID和Common Name

代谢组学数据清洗:用Formula与INCHIKEY精准匹配HMDB ID和Common Name

代谢组学数据清洗这件事,说难不难,说简单也真不简单。我最早接触这批需求,是在处理一批非靶向代谢组学的注释结果时:手里攥着几百上千条化合物记录,每条都带着 Formula(分子式)和 INCHIKEY,但下游分析要用的 HMDB ID 和 Common Name 却是空的。手工一条条去查?不现实。写脚本硬匹配?又会撞上一堆坑——分子式对上了但结构不对、INCHIKEY 前 14 位相同却分属不同条目、同一个化合物在不同数据库里叫法五花八门。这篇就围绕"用 Formula 值匹配 INCHIKEY,再拿到对应 HMDB ID 和 Common Name"这条链路,把我踩过的坑、验证过的思路和能直接抄的代码完整拆一遍,适合做代谢组学、天然产物分析、化合物数据库整合的朋友参考,新手能照着跑通,老手也能在细节里找到几个省时间的点。

1. 先搞清楚这条匹配链路到底在解决什么问题

1.1 Formula、INCHIKEY、HMDB ID 三者各自的角色

很多人一上来就想写 SQL 或者 pandas 的 merge,结果发现字段根本对不上。问题出在没理清这三个字段的语义层级。Formula(分子式)是最粗的一层,它只描述原子组成,比如 C6H12O6,对应的可能是葡萄糖、果糖、半乳糖等一大堆同分异构体。INCHIKEY是 InChI 的哈希压缩形式,标准长度 27 位,前 14 位编码的是分子骨架连接性(也就是"这个分子长什么样"),后 10 位编码的是立体化学、同位素、电荷等细节,最后一位是校验位。HMDB ID则是人类代谢组数据库里的唯一主键,形如 HMDB0000122,它绑定的是一个具体的数据库条目,附带 Common Name、分类、生物样本浓度等一堆元数据。

所以这条链路的本质是:用分子式做粗筛,用 INCHIKEY 做精确定位,最终落到 HMDB 条目上取名字。分子式只是入口,INCHIKEY 才是真正的身份凭证。我见过不少人直接拿 Formula 去 HMDB 里模糊搜,结果一个 C6H12O6 返回几十条,根本没法自动判断该选哪个——这就是跳过 INCHIKEY 的代价。

1.2 为什么不能只用 Formula 直接查 Common Name

举个实际场景。你有一批数据,Formula 列写着 C10H16N5O13P3,这是 ATP 的分子式。但如果你只拿这个分子式去匹配,数据库里可能同时存在 ATP、dATP 的近似条目、各种磷酸化衍生物,甚至一些分子式相同但结构完全不同的代谢物。Common Name 一旦匹配错,下游的通路富集分析、生物标志物筛选全部跟着错,而且这种错误极其隐蔽——名字看起来都像那么回事。

INCHIKEY 的价值就在这里:它是结构决定的,同一个 INCHIKEY 前 14 位基本锁定了分子的连接方式,能有效排除同分异构体的干扰。所以正确的顺序永远是Formula 辅助校验 + INCHIKEY 主键匹配,而不是反过来。我在实际项目里会把 Formula 当作一道"防呆检查":如果某条记录的 INCHIKEY 匹配到了 HMDB 条目,但该条目的分子式和输入的 Formula 对不上,那这条匹配就要打上可疑标记,人工复核。这一步能拦下不少因为 INCHIKEY 录入错误或数据库版本差异导致的误配。

1.3 这条链路典型的应用场景

说几个我实际遇到过的用法。第一类是非靶向代谢组学注释结果的下游整理:质谱软件输出的化合物表里往往只有 Formula 和 INCHIKEY,缺 HMDB ID,而后续做通路分析、网络分析又必须要 HMDB ID 做索引。第二类是多数据库交叉验证:你从 PubChem 或 ChemSpider 拿到一批化合物,想映射到 HMDB 体系里看它们的生物学角色。第三类是自建化合物库的标准化:实验室积累的化合物清单字段混乱,需要统一补全标准 ID 和通用名。

这三类场景的共同点是:数据量大、人工不可行、对准确性要求高。所以下面讲的方案不是"能跑就行",而是要考虑可复现、可审计、出错能定位。

2. 数据源怎么选:本地库、在线接口还是现成映射表

2.1 三种数据获取路径的取舍

做 INCHIKEY 到 HMDB 的映射,数据从哪来直接决定了方案的稳定性和速度。我梳理了三条常见路径,各有各的脾气:

路径数据来源优点缺点适用场景
本地 HMDB 结构文件HMDB 官方提供的 XML/SDF 全量下载离线、可批量、字段完整文件大、更新需重新下载、解析成本高大批量、重复性任务
在线化合物接口各类化合物数据库的查询 API实时、无需维护本地库有速率限制、网络依赖、字段不全小批量、临时查询
现成映射表社区整理的 INCHIKEY-HMDB 对照表开箱即用、轻量覆盖不全、版本滞后、来源需甄别快速验证、补充

我的建议是以本地 HMDB 结构文件为主,在线接口为辅。原因很实在:代谢组学项目动辄几千条记录,走在线接口光是速率限制就能把你卡到怀疑人生,而且一旦网络抖动,整个流程就得重跑。本地库虽然初次准备麻烦点,但一次建好,后面所有项目都能复用,长期看时间成本低得多。

2.2 本地 HMDB 结构文件的字段结构

HMDB 官方提供的结构文件里,每条代谢物记录通常包含这些关键字段:HMDB ID、Common Name、Chemical Formula、Average Molecular Weight、SMILES、InChI、InChIKey、Synonyms 等。解析的时候重点抓四个:accession(即 HMDB ID)、name(Common Name)、chemical_formula、inchikey。

这里有个容易忽略的细节:同一个 HMDB 条目可能对应多个 INCHIKEY,尤其是带立体异构的化合物,数据库里会记录不同立体形式的 INCHIKEY。反过来,同一个 INCHIKEY 前 14 位可能对应多个 HMDB 条目(不同立体化学或不同盐形式)。所以建索引的时候,我通常建两层:一层用完整 27 位 INCHIKEY 精确匹配,一层用前 14 位骨架匹配做兜底。两层配合,既能保证精度,又能在完整键对不上时给出候选。

2.3 在线接口作为补充时的注意事项

在线接口不是不能用,但要清楚它的边界。第一,速率限制必须做请求间隔和重试机制,我一般设 0.2 到 0.5 秒的间隔,遇到限流就指数退避。第二,返回字段可能不全,有些接口只给 HMDB ID 不给 Common Name,那就得二次查询。第三,结果要缓存,同一个 INCHIKEY 查过一次就落本地,避免重复请求。这三点做到了,在线接口作为补充手段是够用的。

提示:无论用哪种数据源,都要记录数据版本和获取时间。代谢组学数据库会更新,今天匹配上的条目明天可能被合并或修订,没有版本记录的话,几个月后你根本说不清当时的结果是怎么来的。

3. 匹配逻辑的核心设计:从精确到模糊的分层策略

3.1 第一层:完整 INCHIKEY 精确匹配

这是最理想的情况。输入的 INCHIKEY 是完整 27 位,本地库里也有完全一致的记录,直接一对一映射,拿到 HMDB ID 和 Common Name。这一步的代码逻辑非常简单,本质就是一个字典查找或者 DataFrame 的 merge。

但实际数据里,完整 27 位能精确命中的比例往往没有想象中高。原因有几个:输入数据的 INCHIKEY 可能被截断、可能带了多余的空格或大小写不一致、可能是旧版本算法生成的。所以匹配前必须做标准化清洗:去首尾空格、统一转大写、检查长度是否为 27、校验最后一位校验位。我见过太多因为一个尾随空格导致整批匹配失败的案例,这种坑完全可以在预处理阶段消灭。

3.2 第二层:前 14 位骨架匹配做兜底

当完整键对不上时,退而求其次用前 14 位匹配。前 14 位锁定的是分子连接性,能保证"骨架相同",但立体化学可能不同。这时候匹配结果可能是一对多,需要额外逻辑来裁决。

我的处理方式是:前 14 位匹配到的所有候选,再用 Formula 做一次交叉验证。如果候选条目的分子式和输入 Formula 一致,保留;不一致的剔除。如果交叉验证后还剩多个候选,就标记为"多候选待定",输出所有候选的 HMDB ID 和 Common Name 供人工判断,而不是武断地取第一个。这一点很重要——自动流程最怕的就是"看起来成功了但其实是错的",宁可标记待定,也不要制造假阳性。

3.3 第三层:Formula 反向校验与异常标记

前面说过 Formula 是防呆检查,具体怎么落地?我的做法是在匹配完成后,对每一条成功映射的记录做一次反向核对:拿匹配到的 HMDB 条目的分子式,和输入的 Formula 做字符串比对。这里要注意分子式的书写规范问题——元素顺序、大小写、括号使用都可能不一致,比如 C6H12O6 和 H12C6O6 在字符串层面不等但化学上等价。

所以比对前要做分子式规范化:解析成元素计数字典,再按标准顺序(通常是 C、H 然后其他元素字母序)重新拼接。规范化之后比对,能消除大部分书写差异带来的假阴性。对于比对不上的记录,打上formula_mismatch标记,单独输出一个异常清单。这个清单在实际项目里非常有用,往往是数据录入错误或者数据库版本差异的线索。

3.4 匹配优先级的完整决策流程

把三层逻辑串起来,完整的决策流程是这样的:

  1. 输入记录先做 INCHIKEY 标准化(去空格、转大写、长度校验)。
  2. 用完整 27 位键查本地索引,命中则直接取 HMDB ID 和 Common Name,标记match_level = exact。
  3. 未命中则用前 14 位查骨架索引,得到候选集。
  4. 候选集用规范化后的 Formula 过滤,剩一条则标记match_level = skeleton,剩多条则标记match_level = ambiguous并输出全部候选。
  5. 所有命中记录做 Formula 反向校验,不一致的打formula_mismatch标记。
  6. 完全未命中的记录标记match_level = none,进入人工或在线接口补充流程。

这个流程的好处是每一步的匹配质量都有明确标记,后续做统计分析时可以按 match_level 分层处理,比如只保留 exact 和 skeleton 级别的结果做严格分析,ambiguous 的单独讨论。

4. 用 Python 把整条链路跑通:可复现的实操代码

4.1 环境准备与依赖

这套流程用 Python 就够了,核心依赖不多:pandas处理表格,lxml或xml.etree解析 HMDB 的 XML 文件,requests用于可选的在线补充。装依赖这一步没什么好省的,直接上:

pip install pandas lxml requests

如果你的 HMDB 结构文件是 SDF 格式,还需要rdkit来解析,但纯做 INCHIKEY 映射的话,XML 版本字段更规整,我更推荐用 XML。SDF 解析虽然灵活,但字段提取要写不少样板代码,对只想拿 ID 和名字的场景属于杀鸡用牛刀。

4.2 解析 HMDB 结构文件建本地索引

假设你拿到的是 HMDB 的代谢物 XML 文件,结构大致是每条<metabolite>节点下挂着<accession>、<name>、<chemical_formula>、<inchikey>等子节点。解析的核心是把这些字段抽出来,建成两个字典索引:

import xml.etree.ElementTree as ET import pandas as pd def build_hmdb_index(xml_path): tree = ET.parse(xml_path) root = tree.getroot() records = [] for metab in root.iter('metabolite'): accession = metab.findtext('accession') name = metab.findtext('name') formula = metab.findtext('chemical_formula') inchikey = metab.findtext('inchikey') if inchikey: inchikey = inchikey.strip().upper() records.append({ 'hmdb_id': accession, 'common_name': name, 'formula': formula, 'inchikey': inchikey }) df = pd.DataFrame(records) # 完整键索引 exact_index = {} for _, row in df.iterrows(): if row['inchikey'] and len(row['inchikey']) == 27: exact_index.setdefault(row['inchikey'], []).append(row) # 骨架索引(前14位) skeleton_index = {} for _, row in df.iterrows(): if row['inchikey'] and len(row['inchikey']) >= 14: skeleton_index.setdefault(row['inchikey'][:14], []).append(row) return df, exact_index, skeleton_index

这里用setdefault配合列表,是因为一个键可能对应多条记录,必须保留全部候选而不是覆盖。这一点在骨架索引上尤其关键,前 14 位一对多是常态。

4.3 分子式规范化函数

前面强调过分子式比对前要规范化,具体实现是把分子式解析成元素计数字典再重排。这里要处理括号和嵌套的情况,比如 Ca(OH)2 这种。写一个能处理常见情况的解析器:

import re from collections import defaultdict def normalize_formula(formula): if not formula: return None formula = formula.strip() # 处理括号展开 def expand(f): # 简单括号处理,支持一层嵌套 pattern = re.compile(r'\(([^()]+)\)(\d*)') while pattern.search(f): f = pattern.sub(lambda m: m.group(1) * int(m.group(2) or 1), f) return f expanded = expand(formula) tokens = re.findall(r'([A-Z][a-z]?)(\d*)', expanded) counts = defaultdict(int) for elem, num in tokens: counts[elem] += int(num) if num else 1 # 标准顺序:C、H 优先,其余字母序 ordered = [] for e in ['C', 'H']: if e in counts: ordered.append(f"{e}{counts[e] if counts[e] > 1 else ''}") for e in sorted(k for k in counts if k not in ('C', 'H')): ordered.append(f"{e}{counts[e] if counts[e] > 1 else ''}") return ''.join(ordered)

这个函数不是万能的,遇到非常复杂的嵌套括号或水合物写法可能出错,但对代谢组学里绝大多数分子式够用了。实际使用时,如果规范化失败(返回 None 或抛异常),就把该条记录标记为formula_parse_error,不要让它静默通过。

4.4 主匹配函数与结果输出

把索引和规范化函数组合起来,写主匹配逻辑:

def match_records(input_df, exact_index, skeleton_index): results = [] for _, row in input_df.iterrows(): raw_key = str(row.get('INCHIKEY', '')).strip().upper() input_formula = normalize_formula(row.get('Formula', '')) result = { 'input_inchikey': raw_key, 'input_formula': row.get('Formula', ''), 'hmdb_id': None, 'common_name': None, 'match_level': 'none', 'formula_mismatch': False } if len(raw_key) == 27 and raw_key in exact_index: cand = exact_index[raw_key][0] result.update({ 'hmdb_id': cand['hmdb_id'], 'common_name': cand['common_name'], 'match_level': 'exact' }) if input_formula and normalize_formula(cand['formula']) != input_formula: result['formula_mismatch'] = True elif len(raw_key) >= 14 and raw_key[:14] in skeleton_index: cands = skeleton_index[raw_key[:14]] if input_formula: filtered = [c for c in cands if normalize_formula(c['formula']) == input_formula] else: filtered = cands if len(filtered) == 1: result.update({ 'hmdb_id': filtered[0]['hmdb_id'], 'common_name': filtered[0]['common_name'], 'match_level': 'skeleton' }) elif len(filtered) > 1: result['match_level'] = 'ambiguous' result['candidates'] = ';'.join( f"{c['hmdb_id']}|{c['common_name']}" for c in filtered) results.append(result) return pd.DataFrame(results)

输出结果里match_level和formula_mismatch两个字段是后续人工复核的关键抓手。我一般会把结果按 match_level 分组导出成多个 sheet,exact 和 skeleton 的直接用,ambiguous 和 none 的单独处理。

4.5 结果验证:抽样回查与统计

代码跑完不代表结束,必须做验证。我的习惯是按 match_level 分层抽样,每层随机抽 20 到 30 条,拿 HMDB ID 去数据库里回查,确认 Common Name 和 Formula 都对得上。同时统计各层的占比,如果 exact 占比异常低(比如低于 30%),就要怀疑输入数据的 INCHIKEY 质量或者本地库版本是否匹配。

还有一个统计值得看:formula_mismatch的比例。如果这个比例超过 5%,说明要么输入 Formula 有系统性问题(比如格式不统一),要么本地库和输入数据来自不同版本,需要排查。这些统计数字不是走形式,它们是判断整批数据可信度的直接依据。

5. 那些文档里不会写的坑:我踩过的五个典型问题

5.1 INCHIKEY 大小写与隐藏字符

这是最高频的坑,没有之一。从 Excel 复制出来的 INCHIKEY 经常带不可见的尾随空格或者制表符,肉眼完全看不出来,但字符串比对就是失败。更隐蔽的是大小写混用,有些软件输出小写,有些输出大写,而字典查找是大小写敏感的。解决办法只有一个:匹配前统一strip().upper(),并且用正则检查是否只含合法字符。我现在的习惯是在预处理阶段就打印出所有长度不等于 27 的记录,逐条看,往往能揪出一批脏数据。

5.2 同分异构体导致的骨架匹配歧义

前 14 位匹配一对多是设计内的行为,但歧义怎么裁决是个真问题。我遇到过一批糖类化合物,骨架相同、Formula 相同,但立体构型不同,前 14 位完全一样,Formula 也过滤不掉。这种情况下任何自动裁决都是猜,正确做法是标记 ambiguous 并输出全部候选,让懂化学的人来判断。强行取第一个的后果是引入系统性错误,而且因为名字看起来合理,极难被发现。

5.3 分子式书写顺序不一致引发的假阴性

C6H12O6 和 H12C6O6 化学上等价,字符串比对却不等。如果不做规范化,反向校验会误报大量 formula_mismatch,把本来正确的匹配标记成可疑。这个坑的教训是:任何涉及化学式比对的环节,都必须先规范化。规范化函数要能处理元素顺序、括号、数字省略(单个原子不写 1)这几种常见差异。

5.4 数据库版本差异导致的 ID 失效

HMDB 会定期更新,条目可能被合并、拆分或废弃。你三个月前建的本地索引,今天拿来匹配新数据,可能就有条目对不上了。更麻烦的是有些旧 ID 在新版本里已经不存在,但输入数据里还带着。我的应对是在索引里保留条目的状态字段(如果源文件有的话),匹配时如果命中已废弃条目,标记出来提醒更新。同时记录本地库的版本号和下载日期,写进结果文件的元数据里。

5.5 一对多映射时的输出设计

一个输入 INCHIKEY 可能对应多个 HMDB 条目(不同盐形式、不同立体化学),这时候输出格式的设计很关键。如果只输出一个字段,多候选信息就丢了;如果拆成多行,下游统计又会重复计数。我的做法是主表保持一行一条输入记录,多候选信息压缩到一个字段里用分隔符拼接,需要展开时再单独解析。这样既保证主表行数稳定,又不丢信息。

6. 匹配质量的分层处理与人工复核策略

6.1 按 match_level 分层导出与优先级

跑完匹配后,我从不把结果一股脑丢给下游,而是按 match_level 分层导出。exact 级别的结果可信度最高,可以直接进入分析流程;skeleton 级别的次之,建议抽样验证后使用;ambiguous 和 none 的必须人工介入。这种分层不是形式主义,它让整个流程的可审计性大大提升——出了问题能快速定位是哪一层出的错。

具体操作上,我会生成一个汇总统计表,列出各 match_level 的记录数、占比,以及 formula_mismatch 的数量。这个表放在结果文件的第一页,任何人拿到数据先看这个表,就能对整体质量有个判断。

6.2 ambiguous 候选的人工裁决辅助

对于 ambiguous 的记录,光给候选列表还不够,得给裁决提供依据。我会在候选信息里额外附上每个候选的分子量、SMILES(如果有的话),方便复核人员快速判断。如果候选数量太多(比如超过 5 个),说明前 14 位骨架太通用,这时候建议改用完整键或者结合其他字段(如保留时间、质荷比)来缩小范围。

人工裁决的结果要回写到主表,并且记录裁决人和裁决依据。这一步在团队协作里尤其重要,否则过几个月没人记得当时为什么选了某个候选。

6.3 未匹配记录的补充查询流程

match_level 为 none 的记录,先别急着放弃。我的流程是:先用在线接口按 INCHIKEY 查一遍,如果在线也查不到,再用 Formula 加分子量做模糊查询,看是否有近似条目。在线查询的结果同样要缓存和记录来源。如果所有途径都失败,就标记为unresolved,在报告里单独说明数量,而不是悄悄丢掉——未匹配记录的比例本身就是一个数据质量指标。

7. 把这套流程用顺之后的几点体会

这套 Formula 加 INCHIKEY 匹配 HMDB 的流程,我从最早的手忙脚乱到现在能稳定批量跑,中间最大的转变不是代码写得多漂亮,而是对数据质量的敬畏。刚开始我总想着一步到位全自动,结果被各种脏数据教做人。后来想明白了:自动化的目标不是消灭人工,而是把人工集中在真正需要判断的地方。exact 匹配交给机器,ambiguous 和异常交给人工,各司其职,整体效率和准确性反而都上去了。

另外一个体会是,索引和规范化这两个基础环节值得多花时间打磨。索引建得好,匹配逻辑就简单;规范化做得全,假阴假阳就少。很多人急着写匹配主逻辑,结果基础不牢,后面全在打补丁。我现在的习惯是先把数据源摸清楚、把字段清洗规则定死、把索引结构设计好,再动手写匹配,前期多花的两小时能省后面两天的调试。

最后分享一个小技巧:每次跑完批量匹配,我都会把结果和上一批的统计数字对比一下。如果 exact 占比突然下降或者 formula_mismatch 突然上升,多半是输入数据或者本地库出了问题,这时候停下来查比闷头往下跑划算得多。数据清洗这活儿,慢就是快。

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进