代谢组学数据整理这件事,说难不难,说简单也真不简单。我接触过的不少做代谢组、天然产物分析的朋友,手里攥着一堆化合物列表,Formula 列、InChIKey 列、HMDB ID 列、Common Name 列各占一栏,看着挺规整,真要对齐的时候才发现——Formula 相同的化合物一抓一大把,光靠一个分子式根本没法唯一定位到某个具体的代谢物。这时候就得靠 InChIKey 这个"分子指纹"来当桥梁,把 Formula 值对应的候选化合物筛出来,再匹配到 HMDB 数据库里的 HMDB ID 和 Common Name。这套流程听起来是三步走,实际做起来坑不少:InChIKey 有 27 位完整版和 14 位骨架版之分,HMDB 的下载文件动辄几百 MB,匹配时大小写、连接层、立体层稍有出入就全军覆没。这篇内容我就把整套"Formula 值 → InChIKey → HMDB ID → Common Name"的匹配逻辑、实操步骤、参数取舍和踩坑经验完整拆一遍,适合做代谢组注释、化合物库整理、质谱数据预处理的朋友直接参考复现。
1. 整体匹配思路与方案选型拆解
1.1 为什么不能只用 Formula 直接查 HMDB
很多人第一反应是:我手里有 Formula,直接去 HMDB 里按分子式搜不就行了?理论上可行,实操上非常痛苦。原因很直接——分子式到化合物的映射是一对多的。以 C6H12O6 为例,葡萄糖、果糖、半乳糖、甘露糖全都是这个分子式,它们的 HMDB ID 各不相同,Common Name 也完全不一样。你拿一个 Formula 去查,返回的是一串候选列表,到底哪个才是你要的那个,光看分子式根本判断不了。
这就是为什么必须引入 InChIKey。InChIKey 是 IUPAC 国际化学标识符的哈希形式,标准版 27 位,前 14 位是"骨架层"(connectivity block),后 12 位是"立体层+同位素层+质子化层"等,最后一位是校验字符。前 14 位相同意味着骨架结构一致,完整 27 位相同才意味着连立体构型都完全一致。所以用 InChIKey 做匹配,等于给分子式加了一道结构层面的精确锁,能把同分异构体区分开。
我一般把整个匹配链路理解成三层过滤:
| 过滤层级 | 匹配字段 | 作用 | 精度 |
|---|---|---|---|
| 第一层 | Formula | 粗筛候选池,缩小范围 | 低,一对多 |
| 第二层 | InChIKey 前14位 | 锁定骨架结构 | 中,可区分构造异构 |
| 第三层 | InChIKey 完整27位 | 锁定立体构型 | 高,可区分立体异构 |
实际做的时候,如果你的输入数据里 InChIKey 是完整的 27 位,那直接走第三层最稳;如果只有 14 位骨架,那就只能停在第二层,接受一定程度的立体异构歧义,这一点后面会细说。
1.2 数据源选择:HMDB 官方结构文件怎么挑
HMDB(人类代谢组数据库)提供了多种下载格式,选错文件会让后续匹配事倍功半。常见的几个文件我列一下对比:
| 文件名(示意) | 内容 | 适用场景 | 体积量级 |
|---|---|---|---|
| hmdb_metabolites.xml | 全量代谢物结构化信息 | 需要完整字段时 | 数百 MB |
| hmdb_metabolites.zip(含SDF) | 结构式+属性 | 需要结构比对 | 数百 MB |
| 血清/尿液等子集 CSV | 特定样本类型代谢物 | 靶向分析 | 数十 MB |
| 代谢物名称与ID对照表 | ID、名称、分子式 | 快速查表 | 较小 |
我的建议是:如果只是做 Formula + InChIKey → HMDB ID + Common Name 的字段映射,优先用包含 InChIKey、Formula、HMDB ID、Common Name 四列的汇总表或 XML 解析后的扁平表,没必要上全量 SDF 去做结构比对,那是另一个量级的工作。XML 文件信息最全但解析成本高,用脚本解析一遍生成扁平 CSV 缓存下来,后续反复查表就快了。
注意:HMDB 不同版本的文件字段命名会有微调,比如 InChIKey 有时列名是
inchikey,有时是InChIKey,读表时统一转小写处理,能省掉大量莫名其妙的匹配失败。
1.3 匹配策略:精确匹配优先,骨架匹配兜底
方案设计上我坚持一个原则:能用完整 InChIKey 精确匹配的,绝不用骨架匹配。因为骨架匹配(前 14 位)会把立体异构体全部召回,比如 D-葡萄糖和 L-葡萄糖骨架相同,你没法区分。但现实是很多上游数据只给了 14 位,或者给的 InChIKey 立体层缺失,这时候就得设计兜底逻辑。
我的做法是双通道匹配:
- 通道 A(精确通道):输入 InChIKey 长度 = 27,直接与 HMDB 的完整 InChIKey 做全等匹配,命中即锁定,唯一性最高。
- 通道 B(骨架通道):输入 InChIKey 长度 = 14,或通道 A 未命中时,取 HMDB InChIKey 前 14 位与输入比对,返回所有骨架一致的候选,再结合 Formula 二次校验。
两个通道的结果要打上标记,精确命中的标exact,骨架命中的标skeleton,方便后续人工复核。这个标记字段在实际项目里非常有用,因为骨架命中的结果往往需要人工确认到底选哪个立体异构体。
2. 核心字段解析与数据预处理要点
2.1 InChIKey 的结构拆解与常见格式陷阱
InChIKey 这 27 位不是随便排的,拆开看有明确含义。以某个标准格式为例,它由三段用连字符隔开:AAAAAAAAAAAAAA-BBBBBBBBBB-C。第一段 14 位是骨架哈希,第二段 10 位是立体/同位素等信息哈希,最后 1 位是校验位。很多匹配失败就败在对这个结构的误解上。
常见的格式陷阱有这么几个:
- 大小写混用:InChIKey 标准是全大写,但有些导出工具会转成小写,或者大小写混杂。匹配前统一
upper()处理,这是最基本的一步。 - 连字符缺失:有些表格导出时把连字符去掉了,变成 27 位连续字符串。这时候要么补回连字符,要么匹配时把两边的连字符都去掉再比。
- 首尾空格与不可见字符:从 Excel 复制粘贴的数据经常带尾部空格或换行符,
strip()一定要做,必要时用正则清洗非字母数字字符。 - 14 位与 27 位混存:同一列里有的行是 14 位骨架,有的是 27 位完整版,长度判断必须先做,否则全等匹配必然失败。
我处理这类数据的标准清洗流程是这样的:
import re def normalize_inchikey(raw): if raw is None: return None s = str(raw).strip().upper() # 去掉所有非字母数字字符(含连字符、空格、换行) s = re.sub(r'[^A-Z0-9]', '', s) if len(s) == 27: # 补回标准连字符格式 return f"{s[:14]}-{s[14:24]}-{s[24]}" elif len(s) == 14: return s else: return None # 长度异常,标记为无效这段清洗逻辑看着简单,但能挡掉实际数据里八成的匹配失败。长度既不是 14 也不是 27 的,一律判为无效并单独记录,不要强行匹配,否则会污染结果。
2.2 Formula 值的规范化处理
Formula 这一列同样不能直接拿来比。分子式的书写有大量等价但字符串不同的形式,比如C6H12O6和C6H12O6看着一样,但有的数据源会写成带空格的C6 H12 O6,有的会把元素顺序打乱成H12C6O6,还有的用下标字符。直接字符串比对是最容易翻车的地方。
规范化的核心是:解析出元素和对应数量,再按固定规则重新拼接。Hill 排序系统是化学领域通用的规则——含碳化合物先写 C,再写 H,其余元素按字母序;不含碳的按字母序全排。按这个规则重新生成分子式字符串,才能保证等价分子式字符串一致。
from collections import defaultdict def parse_formula(formula): # 提取元素符号与数量,支持嵌套括号的简化版 tokens = re.findall(r'([A-Z][a-z]?)(\d*)', formula.replace(' ', '')) counts = defaultdict(int) for elem, num in tokens: counts[elem] += int(num) if num else 1 return dict(counts) def hill_formula(counts): if 'C' in counts: order = ['C'] + (['H'] if 'H' in counts else []) + \ sorted([e for e in counts if e not in ('C', 'H')]) else: order = sorted(counts.keys()) return ''.join(f"{e}{counts[e] if counts[e] > 1 else ''}" for e in order)用 Hill 规则重排后,H12C6O6和C6H12O6都会归一成C6H12O6,匹配成功率立刻上来。这一步在跨数据源整合时尤其关键,因为不同数据库的分子式书写习惯真的不一样。
2.3 HMDB 侧数据的字段对齐
HMDB 下载下来的数据,字段名和你的输入表往往对不上。我一般先做一次字段映射,把 HMDB 侧的列名统一成内部标准名:
| HMDB 原始字段(示意) | 内部标准名 | 说明 |
|---|---|---|
| HMDB_ID / accession | hmdb_id | 主键 |
| name / common_name | common_name | 常用名 |
| chemical_formula | formula | 分子式 |
| inchikey / InChIKey | inchikey | 结构指纹 |
| average_molecular_weight | avg_mw | 平均分子量 |
字段对齐之后,还要做一次去重与空值处理。HMDB 里同一个 InChIKey 理论上应对应一个条目,但实际数据里偶有重复或空 InChIKey 的记录。空 InChIKey 的记录无法参与结构匹配,直接排除;重复的按 HMDB ID 保留一条,避免匹配结果出现一对多膨胀。
提示:HMDB 的 Common Name 有时会有多个别名,主名称字段和同义词字段要分清。做映射时优先取主名称,别名可以另存一列备用,别混进主结果里。
3. 完整实操流程与关键环节实现
3.1 环境准备与依赖安装
整套流程用 Python 就能跑通,核心依赖不多。我习惯用 pandas 做表格处理,用标准库的 re 和 collections 做清洗,如果 HMDB 数据是 XML 格式再额外装个解析库。
pip install pandas lxml数据文件准备两块:一块是你的输入表(含 Formula 和 InChIKey 列),一块是 HMDB 侧的结构汇总表。如果拿到的是 XML,先用脚本解析成扁平 CSV 缓存,后续就不用反复解析大文件了。
import pandas as pd from lxml import etree def hmdb_xml_to_df(xml_path): rows = [] tree = etree.parse(xml_path) for met in tree.iter('metabolite'): rows.append({ 'hmdb_id': met.findtext('accession'), 'common_name': met.findtext('name'), 'formula': met.findtext('chemical_formula'), 'inchikey': met.findtext('inchikey'), }) return pd.DataFrame(rows)解析完存成 CSV,下次直接pd.read_csv读,速度快一个数量级。这一步的取舍逻辑很简单:XML 解析是一次性成本,扁平表是反复使用的资产,把一次性成本摊到缓存里,后续每次匹配都受益。
3.2 输入表与 HMDB 表的双向清洗
清洗环节我前面讲了 InChIKey 和 Formula 的规范化,这里把完整流程串起来。输入表和 HMDB 表都要走同样的清洗函数,保证两边标准一致,这是匹配能对上的前提。
def clean_table(df, formula_col, inchikey_col): df = df.copy() df['formula_norm'] = df[formula_col].apply( lambda x: hill_formula(parse_formula(str(x))) if pd.notna(x) else None ) df['inchikey_norm'] = df[inchikey_col].apply(normalize_inchikey) df['inchikey_skel'] = df['inchikey_norm'].apply( lambda x: x[:14] if x and len(x) >= 14 else None ) return df注意这里同时生成了inchikey_norm(完整版)和inchikey_skel(骨架版)两列。完整版用于精确匹配,骨架版用于兜底匹配,两列并存让后续双通道匹配逻辑写起来很干净。
清洗完要做一次质量统计,看看有多少条 InChIKey 无效、多少条 Formula 解析失败。这个统计不是走过场,它能提前暴露数据质量问题——如果无效比例超过 10%,说明上游数据本身有问题,得先回去查数据来源,而不是硬着头皮往下匹配。
3.3 双通道匹配的核心实现
匹配逻辑是整个流程的心脏。我把它写成先精确、后骨架的两段式,用 pandas 的 merge 实现,效率比逐行循环高得多。
def match_hmdb(input_df, hmdb_df): # 通道 A:完整 InChIKey 精确匹配 exact = input_df.merge( hmdb_df[['inchikey_norm', 'hmdb_id', 'common_name', 'formula_norm']], on='inchikey_norm', how='left', suffixes=('', '_hmdb') ) exact['match_type'] = exact['hmdb_id'].apply( lambda x: 'exact' if pd.notna(x) else None ) # 通道 B:骨架匹配(仅对通道 A 未命中的行) unmatched = exact[exact['match_type'].isna()].copy() skel = unmatched.merge( hmdb_df[['inchikey_skel', 'hmdb_id', 'common_name', 'formula_norm']], on='inchikey_skel', how='left', suffixes=('', '_hmdb') ) skel['match_type'] = skel['hmdb_id'].apply( lambda x: 'skeleton' if pd.notna(x) else None ) # 合并两通道结果 matched = exact[exact['match_type'] == 'exact'] result = pd.concat([matched, skel], ignore_index=True) return result这里有个细节值得说:骨架匹配之后一定要用 Formula 做二次校验。因为理论上骨架相同分子式必然相同,但实际数据里偶有 InChIKey 录入错误导致骨架撞车的情况,加一道 Formula 校验能挡掉这类脏数据。校验逻辑就是比对formula_norm和formula_norm_hmdb是否一致,不一致的降级为suspect标记。
3.4 结果去重与一对多处理
匹配跑完,最头疼的是一对多。一个输入 InChIKey 骨架可能对应 HMDB 里好几个立体异构体,结果表里就会出现多行。处理策略取决于你的下游用途:
- 如果下游需要唯一 ID:精确匹配优先,骨架匹配的多候选按某种规则选一个(比如按 HMDB ID 排序取第一个,或按分子量最接近的选),并记录候选数量供复核。
- 如果下游允许一对多:保留全部候选,加一列
candidate_count标明该输入对应几个候选,人工复核时重点看候选多的。
我一般用聚合的方式生成"主结果 + 候选明细"两张表。主结果每个输入一行,取最优匹配;候选明细保留全部,供追溯。这样既满足自动化下游,又保留了人工核查的余地。
def pick_best(group): # 精确匹配优先,其次按 HMDB ID 排序 group = group.sort_values( by=['match_type', 'hmdb_id'], key=lambda s: s.map({'exact': 0, 'skeleton': 1, 'suspect': 2}) if s.name == 'match_type' else s ) best = group.iloc[0].copy() best['candidate_count'] = len(group) return best main_result = result.groupby('input_id').apply(pick_best).reset_index(drop=True)这段聚合逻辑的关键是排序键的设计——match_type按 exact < skeleton < suspect 的优先级映射成数字,保证精确匹配永远排最前。candidate_count这一列在实际项目里救过我好几次,候选数异常多的行往往意味着数据有问题,值得单独拎出来看。
4. 常见问题排查与避坑经验实录
4.1 匹配率异常低的排查路径
匹配跑完发现命中率只有三四成,别急着怀疑数据,按这个顺序排查基本能定位问题:
| 排查项 | 检查方法 | 典型原因 |
|---|---|---|
| InChIKey 格式 | 统计长度分布 | 连字符缺失、大小写混乱 |
| Formula 书写 | 抽样看原始值 | 元素顺序不同、带空格 |
| HMDB 版本 | 核对下载版本 | 新旧版本 InChIKey 有差异 |
| 字段错位 | 检查列映射 | 列名对上了但内容错列 |
| 空值比例 | 统计非空率 | 上游数据本身缺失严重 |
我遇到最多的是字段错位——列名映射看着对,实际读表时因为分隔符或引号问题导致内容整体偏移一列。这种问题隐蔽性强,排查方法是随机抽几行人工核对原始文件,别只看程序输出的表头。
4.2 InChIKey 立体层差异导致的漏配
有一种漏配特别迷惑人:输入和 HMDB 的 InChIKey 前 14 位完全一样,但完整 27 位对不上,精确通道没命中,骨架通道却能命中。这种情况通常是立体层信息不一致——可能是上游数据生成 InChIKey 时立体构型未定义,导致第二段哈希不同。
处理办法是接受骨架匹配结果,但必须打上skeleton标记,并在结果里保留 HMDB 侧的完整 InChIKey 供比对。千万不要为了追求"精确"而丢弃骨架匹配结果,那等于把本来能用的数据扔了。实际项目里,骨架匹配命中的结果经过人工确认后,绝大多数是可用的。
4.3 一对多候选的人工复核技巧
骨架匹配返回多候选时,人工复核有技巧可循。我一般按这几个维度排序辅助判断:
- 分子量最接近:如果输入数据带分子量,优先选分子量与输入最接近的候选。
- Common Name 语义:结合样本类型(血清、尿液等)和实验背景,选名称最符合语境的。
- HMDB ID 新旧:较新的 HMDB ID 通常对应更规范的条目。
- 候选数量:候选数特别多的(比如超过 5 个),往往说明该骨架在 HMDB 里对应一大类化合物,需要更谨慎。
注意:人工复核这一步不要省。骨架匹配本质上是"可能对",不是"一定对",尤其在代谢组注释场景下,一个错误的 HMDB ID 会传导到下游所有分析,代价很高。
4.4 大批量数据的性能优化
HMDB 全量数据几万到十几万条,输入表如果也是万级,逐行循环匹配会慢到无法接受。我的优化经验是:
- 全程用 merge 代替循环:pandas 的 merge 底层是哈希连接,比 apply 逐行快两个数量级。
- 提前建索引:对 HMDB 表的
inchikey_norm和inchikey_skel建索引,merge 时能进一步加速。 - 分块处理超大输入:输入超过十万行时,按 chunk 分批 merge 再拼接,避免内存爆掉。
- 缓存清洗结果:HMDB 表的清洗结果存成 parquet,下次直接读,省掉重复清洗时间。
hmdb_df.to_parquet('hmdb_clean.parquet', index=False) # 下次直接读 hmdb_df = pd.read_parquet('hmdb_clean.parquet')parquet 比 CSV 读写都快,还保留数据类型,是我处理这类中等规模表格数据的默认选择。
4.5 结果校验与交付前的自检清单
结果表交付前,我固定跑一遍自检,清单如下:
- 每行
match_type是否有值,None的行数是否在可接受范围。 candidate_count分布是否合理,异常大的行是否已人工核查。- 精确匹配行的 HMDB 侧 InChIKey 是否与输入完全一致(防止 merge 串行)。
- Formula 二次校验不一致的
suspect行是否已单独列出。 - 输出表的列顺序、列名是否符合下游约定。
这套自检跑下来,能挡掉绝大多数"看起来对、实际错位"的低级错误。我个人在实际操作中的体会是,匹配流程本身写起来不复杂,真正花时间的是数据清洗和结果校验这两头——清洗决定能不能对上,校验决定对不对得准,中间那段匹配逻辑反而是最省心的部分。后续如果数据源扩展到 KEGG 或 PubChem,这套双通道匹配加标记的框架可以直接复用,只需把 HMDB 侧的字段映射换一换就行。