简介:ukbnmr是一款面向UK Biobank队列研究的R语言工具包,专为Nightingale NMR代谢组学生物标志物数据的高效处理而设计,主要服务从事代谢组学、生物标志物挖掘与流行病学分析的科研人员,也适合具备基础R能力的数据分析者。软件包提供extract_biomarkers()函数,可直接读取UK Biobank输出的原始数据集,自动提取NMR生物标志物字段并生成简明列名,同时将基线期与首次重复评估的度量值拆分至多行,便于纵向数据管理;compute_nightingale_ratios()函数支持基于168种生物标志物计算衍生比率,另附nmr_info数据框用于标志物信息查询与注释。压缩包共19个文件,以7个R脚本、4个Rd帮助文档和1个rda数据文件为核心,另含README、DESCRIPTION、LICENSE、NAMESPACE等规约文件,包体整体约18KB,结构紧凑且易读易改。目前已有909人学习,适合希望快速搭建NMR生物标志物处理流程的R用户直接借鉴使用。
1. 做 UK Biobank NMR 数据清洗:为什么我最终选了 ukbnmr 这个 R 包
如果你拿到过 UK Biobank 里 Nightingale NMR 代谢组学的那 168 个生物标志物字段,应该知道什么叫「数据到手先脱层皮」——原始交付文件里字段名是f.23400.0.0这种编号,单位、批次、样本量全散在好几张说明表里,光是把「哪个编号对应哪种脂蛋白亚组分」对齐就要花半天。ukbnmr 这个 R 工具包解决的问题非常具体:把 UK Biobank 交付的 Nightingale NMR 原始数据,从编号字段映射成可分析的生物标志物矩阵,顺带完成样本量统计、单位换算和基础质控。它适合手里已经有 UK Biobank 数据、正卡在「不知道字段怎么对应、不知道哪些样本是有效测量」这一步的分析者,而不是刚接触 NMR 代谢组学的新手。这篇笔记会把资源拆开讲:它能做什么、怎么装、关键函数怎么调、参数怎么设,以及我在真实数据上踩过的几个坑。
2. 先搞清楚 ukbnmr 在整条分析链里的位置:它不是测量工具,是标准化的桥梁
2.1 Nightingale NMR 数据的原始形态与 UK Biobank 交付差异
UK Biobank 的 NMR 数据来自 Nightingale Health 平台,测量的是血浆或血清样本中的脂蛋白亚组分、脂肪酸、氨基酸等代谢物。但 UK Biobank 交付给研究者的时候,不会给你一个现成的biomarker列,而是给你一套编号字段,比如23400到23483之间的若干字段,每个字段代表一个特定的生物标志物,再加上每个字段对应的instance(随访时间点)和array(重复测量批次)。更麻烦的是,UK Biobank 的「数据展示层」用的是f.xxxxx.0.0这种格式,而 Nightingale 官方文档里用的是原始协议名,比如VLDL_P_L、Total_TG。这两个命名体系之间没有公开的官方对照表,至少英国时间 2023 年前后没有一份随手可下载的 CSV 能直接丢进 R。ukbnmr 的核心贡献就是把这层映射关系固化成了包内的查找表,让研究者不用自己拿两份 PDF 对着抄。
我一般会这样理解它在流程中的位置:原始 UK Biobank 数据 → ukbnmr(字段映射 + 单位统一 + 样本量统计) → 自写的质控脚本 → 下游统计模型(线性回归、GWAS、机器学习)。ukbnmr 并不做代谢物浓度的定量校准,那需要 Nightingale 的原始质控标准曲线;它做的是「交付数据 → 分析可用数据」的转换层。
2.2 它内置了哪三类核心数据资源
这个包含三个基础数据对象,装好之后可以直接用data(package = "ukbnmr")查看。第一个是字段映射表,负责在 UK Biobank 编号和 Nightingale 生物标志物短名之间互转;第二个是单位换算因子表,因为 UK Biobank 交付的浓度单位可能是 mmol/L、μmol/L、g/L 混着来,而 Nightingale 官方建议的统一单位是不同标志物各异;第三个是标志物分组表,把 168 个标志物按「脂蛋白亚组分」「脂肪酸」「氨基酸」「糖酵解相关」「炎症标志物」等类别分组,方便后面批量做类别层面的汇总分析。
还有一个容易被忽略的是样本层面的质控标记逻辑。UK Biobank 对每个 NMR 字段会标注测量是否成功,有些字段值是NaN,有些是0,有些是异常大值。ukbnmr 提供函数判断哪些样本在某类标志物上有完整的有效测量,这个对后续做「完整样本子集」分析特别有用,因为不同指标的缺失模式差异很大,直接na.omit会把样本量砍掉一大半。
2.3 安装方式与 R 版本边界
这个包发布在 CRAN 和 GitHub 上,但两个渠道的版本可能不一致。我的建议是装 GitHub 开发版,因为 CRAN 版更新频率低,字段映射表偶尔会修订。安装命令如下:
# 从 CRAN 安装稳定版 install.packages("ukbnmr") # 从 GitHub 安装开发版(需要先装 remotes) if (!requireNamespace("remotes", quietly = TRUE)) { install.packages("remotes") } remotes::install_github("someone/ukbnmr", ref = "master")注意第二段代码里的someone只是占位符,实际使用时需要替换成真实的 GitHub 仓库地址。安装时容易翻车的地方有两个:一是 R 版本低于 4.1 的话,某些依赖包(如dplyr新版本、tidyr的新函数)装不上,建议先sessionInfo()确认 R 版本;二是 macOS 上编译依赖包需要 Xcode Command Line Tools,Windows 上则需要 Rtools,这个没有捷径,只能提前装好。
安装完成后,我通常会先跑一遍自检:
library(ukbnmr) # 检查内置映射表是否加载正常 data("ukb_nightingale_mapping") head(ukb_nightingale_mapping) # 查看所有内置数据对象 data(package = "ukbnmr")如果head()能正常输出几行映射记录(例如编号字段、短名、R 友好列名),说明安装没问题。常见的坑是包名大小写写错,library(Ukbnmr)会报there is no package called 'Ukbnmr',这种错误很蠢但真会发生。
3. 把原始编号字段转成分析矩阵:核心函数与参数拆解
3.1 数据导入格式:宽表和长表的两种处理路径
UK Biobank 数据最常见的出口格式是 SPSS、SAS 或 CSV。从 UK Biobank 数据展示工具下载后,你拿到的是宽表:每一行是一个参与者(eid),每一列是一个字段实例,例如f.23400.0.0表示参与者第 0 次随访时的字段 23400。ukbnmr 的核心函数期望的输入就是这种宽表,但是列名必须保持 UK Biobank 原始风格——带f.前缀和三个数字后缀。
如果你手里的数据是长表(每个观测是一行,包含eid、field、instance、array、value),需要先转成宽表再喂给 ukbnmr。我遇到过拿长表直接跑的,结果函数匹配不到列名,报错信息还特别含糊。标准做法是用tidyr::pivot_wider转置:
library(tidyr) long_data <- read.csv("nmr_long.csv") wide_data <- long_data %>% pivot_wider( id_cols = eid, names_from = c(field, instance, array), names_sep = ".", values_from = value )转置之后列名会变成23400.0.0这种没有f.前缀的格式,所以还要手动加前缀:
names(wide_data)[-1] <- paste0("f.", names(wide_data)[-1])这里[-1]是为了跳过第一列eid。如果你的长表里同一个参与者同一个字段有多个重复测量,pivot_wider会报重复行错误,需要提前按instance和array聚合或去重。这算是我实际处理时遇到最多的小障碍。
3.2 核心函数map_ukbnmr_fields的行为特征
ukbnmr 包里最常用的函数是字段映射和重命名。假设宽表已经导入为dat,第一列是eid,后续列都是f.开头,那么调用方式如下:
library(ukbnmr) library(dplyr) dat_mapped <- dat %>% map_ukbnmr_fields() %>% select(eid, starts_with("biomarker_"))这个函数做的事情是:识别列名中的字段编号,对照内置映射表,把f.23400.0.0这种列名改写成biomarker_xxx这种语义化列名,同时把不认识的列保留原样(但会抛出一个 warning 提示哪些字段编号不在映射表内)。参数上不用额外配置,它默认使用包内置的映射版本。
关键点是:这个函数不会做单位换算,也不会丢弃异常值。它只做「命名映射」和「筛选有效字段」。如果你把单位换算也指望它,后面会发现结果里浓度数值和 Nightingale 官方单位对不上。单位换算需要单独调用另一个换算函数,我下面会讲。
3.3apply_units单位换算与calculate_concentrations的边界
Nightingale 原始输出里有些标志物是以摩尔浓度(例如 mmol/L)给出,有些是质量浓度(例如 g/L),还有少数是比率或百分比。UK Biobank 交付时基本上统一成了摩尔浓度,但并非全部。ukbnmr 里提供换算函数,用法为:
dat_units <- dat_mapped %>% apply_units()它会根据内置换算表把到手的值全部换算为 Nightingale 推荐单位。一个容易让人误解的地方是calculate_concentrations这个函数的存在意义——它不是用来做生化计算的,而是专门用于处理那些 UK Biobank 只交付了「原始信号强度」或「峰面积」的标志物,需要根据标准曲线换成浓度。但 UK Biobank 交付的 NMR 数据已经是浓度值,所以大多数场景下根本用不到它。我一开始以为每个标志物都要跑一遍这个函数,后来发现纯属浪费算力,还容易把值放大几个数量级。什么时候用?只有当你手头拿到的是 Nightingale 原始仪器导出格式(不是 UK Biobank 交付格式)时,才需要考虑这个。
3.4 样本量统计与完整测量判断:get_available_biomarkers的实用价值
真正的痛点在于「哪些样本有完整数据」。NMR 测量不是每个样本都成功的,有些样本的血浆质量不行,某些指标直接缺失。如果你直接跑模型,R 的lm()会按列表删除缺失行,但你可能根本不知道删了多少、删的是谁。
# 判断每个样本在哪些标志物类别上有完整测量 dat_avail <- dat_units %>% compute_availability() # 查看整体可用性摘要 summary(dat_avail)这个函数会为每个标志物生成一个逻辑向量,TRUE表示该样本在此标志物上有效。你会发现在脂肪酸和氨基酸类别上,缺失率通常低于脂蛋白亚组分,因为脂蛋白亚组分对样本质量更敏感。后续分析如果你的某个表型只依赖特定几个标志物,可以按逻辑向量直接选择样本子集,而不是盲目na.omit。
4. 从 UK Biobank 编号到 Nightingale 短名:字段映射对照的实战
4.1 手工对照 vs 包内映射表:为什么别自己做映射
很多分析者一开始会尝试自己做字段映射,方法是从 UK Biobank 的数据字典网页里抓字段描述,再跟 Nightingale 的 biomarker 清单比对。这个思路听起来可行,但实操时会发现几个坑:第一,UK Biobank 数据字典里的字段描述是给人看的,比如「NMR metabolites: VLDL plasma concentration」这种,跟 Nightingale 内部代码VLDL_P_L不是严格一一对应,有多个字段名字相近但含义不同;第二,某些字段在 UK Biobank 里被拆成多个 instance 和 array,但数据字典只展示了部分;第三,UK Biobank 会更新字段定义,你抓取的网页版本可能已经过时。ukbnmr 内置的映射表是某开发者手动整理、做了大量交叉验证的,省掉这个时间花的很值。
4.2 映射表结构拆解:biomarker_id、field_id、instance、array四元组
装好包后,我建议花十分钟把映射表的结构看一遍,这对理解接下来的报错信息至关重要:
data("ukb_nightingale_mapping") str(ukb_nightingale_mapping) View(ukb_nightingale_mapping)这个映射表的每一行对应一个「字段实例-阵列组合」。至少包含以下几列:
| 列名 | 含义示例 |
|---|---|
field_id | 23400 |
instance | 0(随访次数) |
array | 0(重复测量批次) |
biomarker_id | VLDL_P_L |
r_names | 在 R 中可以直接使用的列名 |
注意同一个field_id可能有多行,因为 UK Biobank 对同一个指标在不同随访点(instance 0、1、2)、不同批次(array 0、1)分别存储。如果你只下载了 instance 0 的数据,映射时匹配instance = 0 & array = 0那行。如果你下载了多次随访,那么宽表里会有f.23400.0.1、f.23400.1.0这样的列,映射表也能匹配上。
4.3 映射失败时的典型报错与定位方法
常见场景是:你下载的字段编号在映射表里不存在。报错信息通常是The following field ids were not found in the mapping table: 23484 23485。这种时候先别慌,有几种可能:
第一种可能是 UK Biobank 后来新增了字段,而这些新字段是 Nightingale 平台后来补充的脂蛋白指标,映射表没收录。这种情况需要你自己手动补映射,做法是先查 UK Biobank 数据字典确认字段描述,再查 Nightingale 官方文档找到对应 biomarker 代码,然后在映射表末尾追加一行。
第二种可能是你把数据列名搞错了,比如少写了f.前缀,导致函数解析不到编号。检查方式是打印几个列名看有没有拼写错误:
names(dat)[1:10]第三种可能是某个字段虽然存在,但它在整个 UK Biobank 队列中只有极少数样本有值,映射表作者有意没纳入。这种就放弃该字段,或者用ukbnmr提供的「原始编号保留」选项,在映射时让未知列保持原样。
我一般会在映射之前先做一个字段交集检查:
field_ids_in_data <- gsub("^f\\.|\\.[0-9]+\\.[0-9]+$", "", names(dat)[-1]) field_ids_in_map <- unique(ukb_nightingale_mapping$field_id) missing_fields <- setdiff(field_ids_in_data, field_ids_in_map) if (length(missing_fields) > 0) { print(paste("Missing field ids:", paste(missing_fields, collapse = ", "))) }这一步能提前发现问题,而不是等map_ukbnmr_fields跑到一半才抛 warning。
5. 避坑与常见问题:NMR 数据处理中最容易翻车的四个地方
5.1 单位换算被重复执行导致数值膨胀
现象:用apply_units()之后,某些标志物的数值比 Nightingale 官方报告值大了近千倍(例如从 μmol/L 变成了 mmol/L 量级对比混乱)。
原因:UK Biobank 交付的数据已经是按统一规范调整过的单位,但apply_units()的换算表是按 Nightingale 原始输出单位设计的。如果原始交付单位恰好和换算表里的目标单位一致,再乘一次换算因子就会出问题。
解决:先抽查几个已知生物标志物的典型范围。比如总胆固醇在 UK Biobank NMR 数据中一般以 mmol/L 为单位,如果你算出来上千,说明单位被乘错。正确做法是只对不确定单位的字段调用换算,或者用apply_units(force = FALSE)让它先检查已有单位(如果函数支持的话)。我实际的做法是:先保留一份原始数值备份,换算后对比列名的单位后缀,再决定是否保留换算结果。
5.2 随访时间点(instance)被混在一起
现象:分析时把 instance 0 和 instance 1 的数据当作同一个变量,导致每个样本有两行,模型标准误全部偏小。
原因:UK Biobank 在第二个随访阶段对部分参与者重新做了 NMR 测量,所以数据里存在同一个eid的多个 instance。如果你没在导入时设置过滤条件,宽表里就会同时包含f.23400.0.0(instance 0)和f.23400.1.0(instance 1)。ukbnmr 的映射函数默认会把所有 instance 都映射出来,因为它是面向「数据全量交付」场景设计的。
解决:在导入后立即按分析目标选择 instance。如果做基线分析,只保留.0.列;如果做纵向分析,则要显式处理同一参与者的多次测量,而不是让 R 按行堆叠。
5.3 缺失值被标记为 0,直接按数值处理会拉低均值
现象:某个标志物(例如高密度脂蛋白中的胆固醇酯)在 20% 样本中显示为 0,但这 20% 样本中的一部分实际上不是真 0,而是测量失败或低于检测限。
原因:UK Biobank 对 NMR 数据的缺失值处理并不统一,部分字段用NaN,部分字段用 0 表示「未检出」,部分字段用空格表示「无数据」。0 与真实低浓度混淆是最大的坑。
解决:使用compute_availability()判断哪些样本是有效测量,再把0值按标志物本身的分布情况决定是转为NA还是保留。通常做法是:如果标志物的最低非零值远大于 0(比如最小正常值是 0.5),那 0 几乎可以肯定是缺失标记。处理代码:
dat_mapped$biomarker_X <- ifelse( dat_mapped$biomarker_X == 0 & !dat_avail$biomarker_X, NA, dat_mapped$biomarker_X )这里dat_avail$biomarker_X是compute_availability()给出的有效测量逻辑向量。
5.4 列名修改后无法同时使用 ukbnmr 后续函数
现象:自己写names(dat)[3] <- "tot_chol"这种手动改名后,再调用compute_availability()时出现「找不到列 biomarker_xxx」的错误。
原因:ukbnmr 期望所有处理过的列名遵循特定命名规则,它内部通过正则表达式识别哪些列是标志物列。手动改成人类可读的短名,比如tot_chol,会导致它的函数无法识别该列。
解决:尊重包内部的列名约定。如果你需要可读性更好的名字,建议在完成所有 ukbnmr 操作之后再批量重命名。要么用rename()做最后一步,要么保留两套空格:原始列名用于函数调用,显示名单独存一份字典。
6. 进阶用法:把 ukbnmr 输出接进 GWAS 或机器学习前的标准化流程
当字段映射和单位问题解决后,真正的分析才刚开始。我这里给出一套我常用的流程,用于把 ukbnmr 的输出干净地喂给下游工具。
第一步:确认分析样本集和标志物类别。只保留目标类别(比如「脂蛋白亚组分」),过滤掉那些缺失率超过 30% 的标志物,避免后续共线性诊断时矩阵奇异:
library(ukbnmr) library(dplyr) dat_analysis <- dat_units %>% compute_availability() %>% mutate(across(starts_with("biomarker_"), ~ ifelse(.x == 0 & is.na(.x), NA, .x))) %>% select(eid, starts_with("biomarker_lipo"))这里starts_with("biomarker_lipo")假设包内脂蛋白类标志物的列名前缀是biomarker_lipo,实际前缀名需要在映射表里查看。
第二步:针对标志物矩阵做逆正态变换(rank-based inverse normal transformation),这是常见做法,因为很多下游工具假设残差正态分布。每个标志物单独排序后映射到正态分布的分位数:
int_transform <- function(x) { qnorm((rank(x, na.last = "keep") - 0.5) / sum(!is.na(x))) } dat_transformed <- dat_analysis %>% mutate(across(starts_with("biomarker_"), ~ int_transform(.x)))建议在变换前先剔除缺失率过高的列,否则rank处理全缺失列时会返回NaN,后面回归会报错。
第三步:如果需要做人 batch 校正(UK Biobank 的 NMR 数据存在批次效应,尤其在不同测量批次之间),可以用线性回归剔除 plate 或 batch 效应。但是 UK Biobank 交付数据里通常不直接给出 plate 编号,需要用f.字段里的 array 信息近似,或者用参与者招募中心作为协变量。这个做法比较粗,但至少能消除一部分系统性偏移。
第四步:把标准化后的数据写出为 CSV 或 RDS,供 GWAS 软件(如 plink 的--pheno格式)或 Python 机器学习框架读取:
write.table( dat_transformed, file = "nmr_transformed.txt", row.names = FALSE, quote = FALSE, sep = "\t" )注意plink要求表型文件第一列为 FID、第二列为 IID,所以需要额外准备一个映射文件,把eid拆成两列。这个细节经常被人忽略,导致表型文件加载后所有样本都被过滤掉。
整个流程下来,我最大的教训是不要跳过「有效性判断」这一步直接做数值变换。有一次我为了省事,直接拿原始值跑了一个线性回归,结果关联信号全部指向同一批缺失样本——因为缺失模式本身就与年龄和性别相关,造成了严重混淆。从那以后,我每次拿到新的 UK Biobank NMR 数据都会强制走一遍compute_availability(),先画出缺失模式图,再谈分析。这个习惯帮我避开了很多后续的返工,希望也能帮到你。
如果你最终决定用这个资源,记得先检查你拿到的 UK Biobank 数据集版本和包内映射表的更新时间是否对得上。如果数据下载时间比映射表更新时间晚,最好去仓库看一下是否有新增字段的补丁。做这类代谢组学数据清洗,细心比聪明重要得多。
本文还有配套的精品资源,点击获取