很多初次接触 NHANES 的读者往往是在文献里看到“基于美国国家健康与营养调查NHANES数据”的表述后想试着复现类似的关联分析结果却卡在第一步数据从哪里下载XPT 文件怎么打开抽样权重到底要不要加读完一堆零散资料后反而更不知道从哪下手。本文围绕这套 NHANES 数据库实战流程展开从原始数据下载、数据清洗、加权统计分析、结果绘图到一篇典型论文核心结果的复现思路按顺序完整拆解。文章会给出可直接运行的 R 代码并解释关键步骤背后的原理。适合公共卫生、临床医学、流行病学方向的研究生也适合想用公开数据库快速完成论文数据分析的初学者。1. 背景与核心概念1.1 NHANES 是什么NHANES 全称是 National Health and Nutrition Examination Survey由美国国家健康统计中心NCHS组织是一项持续运行的全国性横断面调查。它的特点是“月月采样、两年一个周期、数据公开”每个周期会访问数千名受试者收集人口学信息、体格检查、实验室检验、饮食摄入、生活方式问卷等多模块数据。正因如此NHANES 成了医学论文中的“常客”。研究者不需要自己招募患者不需要伦理审批的漫长流程只要登录 CDC 官网就能获取匿名化的样本数据。很多疾病的患病率、营养素与健康结局的关联、环境暴露与代谢指标的关系都能在 NHANES 数据上做一次较为规范的横断面检验。需要区分的是NHANES 不是单一的 Excel 表格而是按模块拆成若干独立数据文件。一个周期里有人口学文件DEMO、血压测量文件BPX、实验室文件GLU、HDL、TCH 等、饮食文件DR1TOT、DR2TOT等。这些文件通过受试者编号 SEQN 关联起来分析时需要自行合并。1.2 为什么能用来做论文复现论文复现不是“照着摘要抄一遍”而是按照已发表文章 Methods 部分的描述重新从原始数据出发尽量复现出它的样本量、主要表格和回归结果。NHANES 的优势在于数据公开任何人可下载周期多变量覆盖广很多常见方向都能找到对应变量官网提供数据字典变量含义清楚有成熟的 R 包和教程可复现性高。复现的价值不只是验证别人的结果而是借一篇真实文献把数据下载、变量选择、加权分析、图表输出这一整条链路跑通。跑通一次之后下次拿到任何公开问卷数据你都知道从哪里下手。1.3 分析前必须理解的四个概念SEQN受试者唯一编号。所有模块文件都靠它匹配相当于数据库主键。XPT 文件SAS transport 格式。RIH 老读者可能见得少很多软件无法直接打开它需要用haven包读取。PSU 和 Strata初级抽样单元Primary Sampling Unit与分层变量。NHANES 采用复杂抽样设计不是简单随机样本所以必须告诉统计软件抽样结构。权重Weight每个受试者代表多少人口数。因为抽样概率不同、无应答率不同分析时要加权才能得到具有全国代表性的估计值。这四个概念不理解后面每一步都可能出错。尤其是权重它是 NHANES 分析与普通数据分析最大的区别。2. 环境准备与版本说明2.1 软件环境本文示例使用 R 语言完成。建议安装 R 4.0 及以上版本和 RStudio 桌面版。操作系统的差异影响不大但路径中的中文名尽量改为英文避免部分包读写文件报错。需要安装的 R 包如下install.packages(c(haven, tidyverse, survey, tableone, ggplot2, nhanesA))haven读取 XPT、SAS、Stata 等格式tidyverse数据清洗常用工具集survey复杂抽样加权分析的核心包tableone快速生成论文中的基线表 Table 1ggplot2结果可视化nhanesA自动从 CDC 下载 NHANES 数据并读入 R。考虑到部分用户网络环境不稳定下面也会介绍手动下载 本地读取的方案。2.2 周期与数据文件说明NHANES 每两年为一个周期例如2017-2018 周期文件后缀通常为_J如DEMO_J.XPT2015-2016 周期后缀_I如DEMO_I.XPT2019-2020 周期及之后命名规则有所变化且发布更加延后。本文示例以 2017-2018 周期为例涉及的变量名以 CDC 官网提供的 Data Dictionary 为准。不同周期变量名可能略有差异实际分析时优先查数据字典不要凭记忆硬编码所有变量。2.3 推荐的项目目录建议在本地建立如下结构nhanes_project/ ├── data/raw # 原始 XPT 文件 ├── data/clean # 清洗后的中间数据 ├── script/ # R 脚本 ├── output/figures # 图表 └── output/tables # 表格这样能避免“所有文件和脚本堆在一个目录”的混乱状态也有利于复现时回溯自己的步骤。3. 数据下载与读取3.1 方式一手动从 CDC 官网下载在浏览器打开 NHANES 官网的数据查询页面选择周期后可以看到该周期全部数据文件列表。以 2017-2018 为例Demographic Data人口学文件文件号 DEMO_JBlood Pressure血压测量文件号 BPX_JFasting Questionnaire空腹问卷FQ_J等。每个文件旁都提供了XPT格式下载链接。点击下载后文件通常是一个 XPT 文件。下载后不再需要手动点任何按钮直接用haven::read_xpt()读取即可。手动下载的好处是稳定、可复现性好适合写论文时说明“数据下载日期和版本”。缺点是要为每个模块单独下载模块一多就比较繁琐。3.2 方式二使用 R 包自动下载nhanesA包封装了 CDC 下载流程可以直接按文件号抓取数据library(nhanesA) # 下载 2017-2018 人口学数据与血压数据 demo - nhanes(DEMO_J) bpx - nhanes(BPX_J) # 查看数据维度 dim(demo) dim(bpx)执行成功后demo和bpx已经是 data.frame可以直接查看变量名names(demo)[1:20]自动下载的优势是脚本可重复执行不需要手动保存文件。缺点是扩展包依赖网络状况且如果你需要多个大文件下载过程中可能偶发超时。此时可以退回手动下载方式。3.3 读取本地 XPT 文件如果你已经手动下载了 XPT 文件使用haven读取library(haven) demo - read_xpt(data/raw/DEMO_J.XPT) bpx - read_xpt(data/raw/BPX_J.XPT) glimpse(demo)glimpse来自dplyr可以快速查看每列的完整信息。首次读取时要重点关注变量类型和大小写。NHANES 变量名通常是大写例如年龄变量是RIDAGEYR性别变量是RIAGENDR。3.4 按 SEQN 合并数据文件不同模块的公共键是SEQN。合并时需要决定保留哪些受试者all FALSE只保留两个文件都存在的个体all.x TRUE保留左侧文件全部个体all TRUE保留全部个体样本量可能膨胀。复现论文时一般先以“纳入标准”确定研究人群再按需合并其他模块。一个常见的做法是dat - merge(demo, bpx, by SEQN, all.x TRUE)这样保留了所有人口学样本血压缺失的人则记为缺失值后续通过filter去掉缺失项。4. 数据清洗与预处理4.1 筛选变量NHANES 原始文件动辄几十甚至上百列。先筛选出本分析真正需要的变量再处理逻辑会更清晰。下面是一个基于select()的示例library(tidyverse) # 保留人口学与抽样设计相关变量 demo_clean - demo %% select( SEQN, RIDAGEYR, # 年龄单位岁 RIAGENDR, # 性别1 男2 女 RIDRETH1, # 种族/民族分类 SDMVPSU, # 初级抽样单元 SDMVSTRA, # 分层变量 WTMEC2YR # MEC 检查权重 ) bpx_clean - bpx %% select( SEQN, BPXOSY1, # 收缩压第一次读数 BPXODI1 # 舒张压第一次读数 )注意变量名以数据字典为准。WTMEC2YR适用于体检项目MEC分析是最常用的两年周期权重。4.2 处理缺失值NHANES 中的缺失值有两类真正缺失变量值就是NA编码 7、8、9 表示“无法测量”“未测量”“不知道”。例如血压变量如果出现 0 或异常编码应结合数据字典判断。简单处理时可以把这些编码统一转为NAbpx_clean - bpx_clean %% mutate( BPXOSY1 ifelse(BPXOSY1 0, NA, BPXOSY1), BPXODI1 ifelse(BPXODI1 0, NA, BPXODI1) )不同文件的缺失编码不完全一致务必以该文件的Codebook为准。4.3 重编码变量性别变量原本是 1/2。很多模型更喜欢 0/1 编码例如 1 表示女性。年龄可以按需要分组。以下示例同时生成高血压结局变量dat - merge(demo_clean, bpx_clean, by SEQN, all.x TRUE) dat - dat %% mutate( female ifelse(RIAGENDR 2, 1, 0), age_cat cut(RIDAGEYR, breaks c(20, 40, 60, 80), labels c(20-39, 40-59, 60-80), right TRUE), hypertension ifelse(BPXOSY1 130 | BPXODI1 80, 1, 0) )cut默认右闭区间(20,40]会把 20 岁放进20-39这一组严格来说20 岁这个边界需要再确认。实际使用时建议用cut(RIDAGEYR, breaks c(20, 39, 59, 79))或者检查边界情况。在本文示例中我们用简单方式演示逻辑最终分组区间应结合论文的定义调整。4.4 排除标准与样本量记录论文复现最容易被忽略的点就是“原文章排除了哪些人”。常见排除条件包括年龄小于 20 岁孕妇关键变量缺失不属于 MEC 检查对象。复现时每一步筛选都应该记录样本量变化。建议在脚本里用注释或打印输出记录# 筛选年龄 20 岁以上 dat1 - dat %% filter(RIDAGEYR 20, RIDAGEYR 80) cat(年龄筛选后样本量, nrow(dat1), \n) # 去除关键结局缺失 dat2 - dat1 %% filter(!is.na(hypertension)) cat(结局无缺失样本量, nrow(dat2), \n) # 去除权重缺失 dat3 - dat2 %% filter(!is.na(WTMEC2YR)) cat(权重无缺失样本量, nrow(dat3), \n)这个过程能帮你快速定位“为什么复现出的样本量和原文不一样”。很多时候不是代码错了而是排除标准没有对齐。5. 加权统计分析5.1 为什么必须加权NHANES 不是简单随机抽样。它的样本设计包括分层、多阶段抽样部分人群还会被过量抽样。如果不加权直接用原始样本算出来的患病率、回归系数会有偏不能代表美国全国总体。加权分析的第一步是把抽样结构告诉 R。核心是survey包。5.2 构建加权抽样设计library(survey) design - svydesign( id ~SDMVPSU, # 初级抽样单元 strata ~SDMVSTRA, # 分层变量 nest TRUE, # PSU 编号在各层内不唯一 weights ~WTMEC2YR, # 两年周期权重 data dat3 ) print(design)nest TRUE是 NHANES 分析中经常需要设置的参数。PSU 编号在不同层之间重复如果不加nest TRUEsurvey包可能会把不同层的 PSU 误判为同一个单位得到不正确的方差估计。5.3 加权描述统计用加权方式计算连续变量均值和分类变量比例# 加权平均年龄 svymean(~RIDAGEYR, design, na.rm TRUE) # 加权性别比例 svytable(~RIAGENDR, design)如果想按亚组计算患病率可以使用svyby# 按年龄组计算高血压加权患病率 prop_by_age - svyby( ~hypertension, by ~age_cat, design design, FUN svymean, na.rm TRUE ) print(prop_by_age)prop_by_age的结果可以直接用于后续绘图。5.4 加权回归模型二分类结局常用的模型是加权 logistic 回归model - svyglm( hypertension ~ RIDAGEYR female RIDRETH1, design design, family quasibinomial() ) summary(model)survey包里对 logistic 回归常建议使用quasibinomial()因为复杂抽样设计下它不强制假设二项分布完全正确对方差估计更稳健。提取系数、标准误、OR 和置信区间coef_model - summary(model)$coefficients ci - confint(model) or_df - data.frame( term rownames(coef_model), OR exp(coef_model[, 1]), lo exp(ci[, 1]), hi exp(ci[, 2]), p coef_model[, 4] ) print(or_df)这里的 OR 是“暴露每增加 1 个单位或相对于参照组的优势比”。如果年龄变量是连续型OR 表示年龄每增加 1 岁的效应如果希望每 10 岁变化展示可以在模型里使用I(RIDAGEYR / 10)。5.5 不同权重的选择NHANES 不止一个权重。常见的有WTMEC2YR体检MEC样本权重绝大多数分析使用WTSAF2YR空腹亚样本权重用于空腹血糖、胰岛素等空腹检验项目WTDR2D饮食数据权重用于首个饮食日回忆样本多年份合并分析时权重通常要除以合并的周期数例如合并两周期时使用WTINT2YR时要注意权重的缩放方式。选择权重的核心原则是你分析的数据来自哪一个子样本就用对应子样本提供的权重。空腹血糖分析绝不能盲目使用WTMEC2YR否则样本代表性会出问题。6. 绘图与图表复现6.1 生成基线表 Table 1复制论文中的 Table 1可以借助tableone包。它支持直接传入svydesign对象从而输出加权后的基线特征library(tableone) vars - c(RIDAGEYR, female, age_cat, hypertension) table1 - CreateTableOne( vars vars, strata hypertension, data dat3, survey design ) print(table1, showAllLevels TRUE, formatOptions list(big.mark ,))strata指定按结局分组这样得到的是“高血压组 vs 非高血压组”的基线表。survey design是关键它让tableone知道当前是加权设计数据。6.2 绘制加权患病率柱状图把svyby的输出转为 data.frame然后用ggplot2绘制library(ggplot2) prop_plot - as.data.frame(prop_by_age) prop_plot$age_cat - rownames(prop_plot) ggplot(prop_plot, aes(x age_cat, y hypertension)) geom_col(fill #4C72B0, width 0.6) geom_errorbar( aes(ymin hypertension - se, ymax hypertension se), width 0.2 ) labs( title Weighted Prevalence of Hypertension by Age Group, x Age Group, y Prevalence ) scale_y_continuous(labels scales::percent) theme_minimal(base_size 14)se是svyby自动计算的标准误列名有时是se有时是se.hypertension。建议画图前先names(prop_plot)确认列名。6.3 绘制森林图流行病学文章里出现频率很高的一个图表是森林图用于展示多项变量的 OR 和 95% CI。这里以模型输出的or_df为基础构造演示数据forest_data - data.frame( term c(Age (per 10 years), Female vs Male, Mexican American vs White), OR c(1.50, 1.12, 0.98), lo c(1.40, 0.98, 0.85), hi c(1.61, 1.28, 1.13) ) ggplot(forest_data, aes(x OR, y term)) geom_point(size 2.5, color #2C3E50) geom_errorbarh( aes(xmin lo, xmax hi), height 0.2, color #2C3E50 ) geom_vline(xintercept 1, linetype dashed, color grey40) labs( x Odds Ratio (95% CI), y NULL ) theme_minimal(base_size 14)注意这里的 OR、lo、hi 是演示数值实际应该从你的回归模型中提取。提取方法在前面已经给出可以直接替换forest_data为真实结果。6.4 图片导出论文投稿时常要求 TIF 或高分辨率 PNG。RStudio 下可以按目标期刊要求设置输出ggsave( output/figures/forest_plot.png, width 8, height 5, dpi 300 )dpi 300基本能满足期刊印刷需求。如果需要导出 TIF可以安装tiff包并指定device tiff不过不同类型期刊要求不同建议投稿前再看具体要求。7. 论文复现一个迷你实战脚本下面把前面所有环节串联成一个完整脚本。这个脚本的目标是“复现”一篇典型横断面研究的结果探讨年龄、性别与高血压患病风险之间的关联并输出加权患病率图与森林图。# 文件路径script/01_hyp_replication.R # 目标基于 NHANES 2017-2018 复现加权 logistic 回归分析 library(haven) library(tidyverse) library(survey) library(ggplot2) # 1. 读取数据 demo - read_xpt(data/raw/DEMO_J.XPT) bpx - read_xpt(data/raw/BPX_J.XPT) # 2. 筛选关键变量 demo_clean - demo %% select(SEQN, RIDAGEYR, RIAGENDR, RIDRETH1, SDMVPSU, SDMVSTRA, WTMEC2YR) bpx_clean - bpx %% select(SEQN, BPXOSY1, BPXODI1) # 3. 合并并生成分析变量 dat - merge(demo_clean, bpx_clean, by SEQN, all.x TRUE) %% mutate( female ifelse(RIAGENDR 2, 1, 0), hypertension ifelse(BPXOSY1 130 | BPXODI1 80, 1, 0) ) %% filter(RIDAGEYR 20, RIDAGEYR 80) %% filter(!is.na(hypertension), !is.na(WTMEC2YR)) cat(最终分析样本量, nrow(dat), \n) # 4. 构建加权设计 design - svydesign( id ~SDMVPSU, strata ~SDMVSTRA, nest TRUE, weights ~WTMEC2YR, data dat ) # 5. 加权 logistic 回归 model - svyglm( hypertension ~ I(RIDAGEYR / 10) female, design design, family quasibinomial() ) summary(model) # 6. 导出 OR 与置信区间 ci - confint(model) res - data.frame( term c(Age (per 10 years), Female vs Male), OR c(exp(coef(model)[2]), exp(coef(model)[3])), lo c(exp(ci[2, 1]), exp(ci[3, 1])), hi c(exp(ci[2, 2]), exp(ci[3, 2])) ) # 这里 coef(model)[1] 是截距通常不展示 print(res) # 7. 简单加权患病率图 prop_data - as.data.frame( svyby(~hypertension, by ~female, design design, FUN svymean, na.rm TRUE) ) ggplot(prop_data, aes(x factor(female, labels c(Male, Female)), y hypertension)) geom_bar(stat identity, fill #4285F4, width 0.55) geom_errorbar(aes(ymin hypertension - se, ymax hypertension se), width 0.15) scale_y_continuous(labels scales::percent) labs(x NULL, y Hypertension Prevalence) theme_minimal(base_size 14)运行这段脚本后你会得到两个核心结果加权 logistic 回归中年龄和性别的 OR 值性别间高血压加权患病率柱状图。要复现某篇具体论文只需要把变量、排除标准和模型调整成论文 Methods 里的描述。如果样本量对不上优先检查排除标准和缺失值处理。8. 常见问题与排查思路问题现象常见原因解决思路读取 XPT 报错haven版本较旧或文件下载不完整重装haven删除原文件后重新下载合并后样本量暴增merge使用了all TRUE改为all.x TRUE或all FALSEsvyglm报权重为 0 或 NA没有过滤缺失权重分析前filter(!is.na(WTMEC2YR))加权患病率与未加权差异很大未加权重或少数组被过量抽样确认所有分析使用同一个svydesign对象亚组回归报“design differs”对子集重新做了未保留抽样结构的filter子集分析时建议用subset(design, 条件)结果与原论文样本量不一致排除标准、缺失值处理不一致逐条核对论文 Methods 的纳入与排除流程8.1subset(design, ...)的用法当我们要按某个条件做亚组分析时不能在 data.frame 上filter后再重新构建svydesign更推荐的做法是直接基于原 design 进行subsetdesign_female - subset(design, female 1) model_female - svyglm( hypertension ~ I(RIDAGEYR / 10), design design_female, family quasibinomial() )这样抽样层次和权重会保持正确。如果直接filter(dat, female 1) %% svydesign(...)也能得到结果但严格来说抽样关系可能被切断方差估计不再准确。8.2 多周期合并的权重调整如果想要合并两个周期例如 2015-2016 与 2017-2018最常被问到的就是权重计算。一般情况下两周期合并仍使用两年的WTMEC2YR权重但样本量翻倍后置信区间可能偏窄。多周期正式分析时需要根据 CDC 官方建议进行权重缩放。不同年代的发布规则可能有更新建议以 CDC 发布的分析指南为准。9. 最佳实践与工程建议9.1 脚本与数据字典同步保存不要把分析结果只存在 R 全局环境里。每个脚本开头注明数据周期、下载日期、变量字典版本让未来读者包括几个月后的自己能重建整个流程。这也是学术复现的基本要求。9.2 产物分层保存前面提到项目目录要分成 raw、clean、figures、tables。清洗完成的中间数据可保存为.rds或.csvsaveRDS(dat, data/clean/nhanes_2017_2018_clean.rds)后续分析直接读取不用每次从头下载和清洗既省时间又避免误改原始数据。9.3 权重处理优先于统计描述各种统计软件对不同权重定义的处理方式不完全一样。在 R 中关键是“加权前先构建设计对象加权后所有描述都基于设计对象”。不要出现“清洗用了权重、描述又用了未加权样本”的混用情况。9.4 缺失值处理要记录决策同一篇论文如果原作者用多重插补而你只做完整案例分析样本量和结果自然不同。复现时应先尝试“完整案例分析”再看是否需要补缺失。每一步对缺失项的处理都应写进代码注释而不是只存在于头脑里。9.5 警惕多重比较问题NHANES 变量非常多如果一次分析同时测试几十个暴露变量会带来多重比较问题。学术写作中需要说明检验次数或者对主要结局使用更严格的标准。这个点在复现论文时尤其重要因为有些原文章只报告了显著变量容易让人忽略检验次数的问题。9.6 数据安全与合规NHANES 是公开匿名数据但仍应遵守 CDC 的数据使用要求。不要公开重新识别个体身份不要擅自把下载数据转售或用于超出授权范围的项目。论文投稿时按照期刊要求引用 NHANES 数据版本即可。10. 总结与学习路线本文从一个刚接触 NHANES 的视角出发完整走了一遍公开数据库发文的基础链路下载数据、读取 XPT、按 SEQN 合并、清洗变量、构建加权设计、做加权回归、生成 Table 1 和森林图。这套流程是一条可以直接复用的“底稿”任何一篇以 NHANES 为基础的横断面研究本质上都是同样的流水线。如果接下来想继续进阶可以往这几个方向延展多周期合并分析例如 2005-2006 到 2017-2018 共多个周期的趋势分析非线性和阈值效应分析比如用限制立方样条RCS探索年龄与某指标的非线性关系缺失数据多重插补与复杂抽样设计的结合用 R Markdown / Quarto 自动生成可复现的分析报告。对于还没动手的读者我的建议是不要等“学完所有 R 知识”再开始。直接从本文第五节的最小脚本开始换一个结局变量例如把高血压换成高血糖诊断你就会遇到真实的数据清洗问题这些问题才是进步最快的地方。下载一份 2017-2018 数据跑通第一个加权模型后整套 NHANES 实战流程就会变得清晰很多。如果本文对你有帮助可以收藏备用也欢迎在练习中遇到具体报错时按第八节的排查表逐项对照。