☰
NHANES数据库实战:从数据下载到加权分析与论文复现
2026/10/22 23:06:01 网站建设 项目流程

做临床科研的人,尤其是暂时拿不到自己医院数据的研究生和青年医生,几乎都绕不过 NHANES 这个数据库。它是美国国家健康与营养调查(National Health and Nutrition Examination Survey)的公开数据,由美国 CDC 下属的 NCHS 负责执行和发布,免费、样本量大、变量覆盖广,既能用来做流行病学关联分析,也能支撑不少 SCI 论文。但真正动手的人会很快发现:收藏夹里存了几套教程,打开 RStudio 后却连第一步“把 XPT 文件读进来”都要报错。

对 NHANES 学习路径,我的判断很明确:它真正的门槛不在统计,而在三条固定链路——能不能下载到正确周期的文件、能不能理解权重/层/簇三个抽样变量、能不能把清洗合并流程标准化。这三件事一旦打通,后面的描述统计、加权回归、绘图和论文复现,基本都是套模板。

这篇文章会按一条可以落地的“一周路线”来写:原始数据下载、数据清洗、统计分析、结果绘图、论文复现。代码以 R 为主,同时给出 Python 读取方案。读完你至少能复现一篇常见关联性分析论文的主干流程,并且知道每一步做完应该看到什么结果、出错时去哪里排查。

1. NHANES 论文复现为什么值得专门学

很多人对 NHANES 的第一印象是“一个可以免费下载的美国健康调查数据”,但这个印象远远不够。它的价值不只在于“数据免费”,而在于数据结构非常稳定、变量体系非常完整、发论文路径非常成熟。

先说稳定性。NHANES 从 1999 年开始采用连续调查模式,每两年一个周期,每期都包含人口学、体测、实验室检测、饮食和问卷五大类数据。这意味着 20 多年里,不同周期的变量命名规则基本一致。你今天学会了 2017-2018 周期的处理方式,明天换到 2013-2014 周期,代码只需要改文件后缀和少量变量名。

再说复现训练的价值。找一篇已发表的 NHANES 论文,尝试复现它的 Table 1 和主回归结果,是学习临床数据分析最高效的方式之一。在这个过程中,你会被迫搞清楚三件事:

  • 作者用了哪个周期的数据、纳入了哪个年龄段;
  • 暴露、结局和协变量分别来自哪个模块文件、怎么定义的;
  • 作者是否用了权重、做了哪种缺失处理。

这三件事恰恰是很多自学科普不会系统讲的地方。等你自己能完整复现一篇论文后,换一个暴露变量、换一个结局指标,就能快速产出一篇新分析。当然,新研究要遵守 NHANES 数据使用说明和期刊要求,不能简单把复现当成“换皮”。

谁适合学这套流程?临床医生、公共卫生专业研究生、想做孟德尔随机化或队列研究但暂时缺少医院数据的人,以及想系统学习复杂抽样数据分析的生统学生。你需要的基础不高:会一点 R 或 Python 的读写数据语法即可,统计知识可以边做边补。

2. NHANES 核心概念:周期、文件、关键变量

2.1 两年一个周期,文件后缀跟随周期号

NHANES 连续调查从 1999-2000 周期开始,每个周期对应一个文件字母,几乎所有 XPT 文件名都会带上这个字母。

调查周期文件字母人口学文件示例说明
1999-2000无后缀DEMO.XPT早期文件没有字母
2001-2002BDEMO_B.XPT
2003-2004CDEMO_C.XPT
2005-2006DDEMO_D.XPT
2007-2008EDEMO_E.XPT
2009-2010FDEMO_F.XPT
2011-2012GDEMO_G.XPT部分种族变量从这期起更完整
2013-2014HDEMO_H.XPT
2015-2016IDEMO_I.XPT
2017-2018JDEMO_J.XPT
2019-2020KDEMO_K.XPT也常合并为 P_ 前缀的 2017-March 2020 文件
2021-2023LDEMO_L.XPT当前最新周期

这里有两个最容易踩的坑。

第一个坑:1999-2000 周期的文件名没有字母。很多新手拿 2001 年以后的命名规则去找“DEMO_A.XPT”,结果 404。第一个周期的文件就叫 DEMO.XPT、BMX.XPT。

第二个坑:2019-2020 周期受特殊情况影响,只收集到 2020 年 3 月,官方同时提供了一组“2017-March 2020”合并文件,文件名前缀是 P_,比如 P_DEMO.XPT。如果论文写的是“NHANES 2017-2020”,作者很可能用的就是这组文件,权重变量也不是 WTMEC2YR,而是专用的 prepandemic 权重。

2.2 数据文件按模块拆分

NHANES 并不是一个大文件,而是按模块发布几十个 XPT 文件。做一篇论文通常只用到其中四到六个。

模块类型含义常见文件常见变量
人口学性别、年龄、种族、收入、教育DEMO_JRIAGENDR, RIDAGEYR, INDFMPIR
检查体测、血压、骨密度BMX_J, BPX_JBMXBMI, BPXSY1
实验室血液、尿液生化指标GHB_J, TCHOL_JLBXGH, LBXTC
问卷自报疾病、饮酒、吸烟DIQ_J, SMQ_J, BPQ_JDIQ010, SMQ020

每个文件旁边通常还有一个“Doc”形式的变量说明页,正式名称叫 Codebook。下载数据前先打开 Codebook,确认你要的变量确实存在,再动手写代码,这是省时间的关键习惯。

2.3 必须背下来的关键变量

无论做什么分析,下面几个变量都会反复出现:

变量作用使用要点
SEQN个体编号所有模块文件合并的 key,每个周期内唯一
SDDSRVYR调查周期编号合并多周期时标记数据来源
RIAGENDR性别1=男,2=女
RIDAGEYR年龄单位是“岁”
RIDRETH3种族/民族2011-2012 周期起使用
INDFMPIR家庭收入与贫困线比值一般取值 0 到 5
WTMEC2YR两年体检权重绝大多数分析使用它
SDMVPSU初级抽样单元survey 设计中的 ids
SDMVSTRA分层变量survey 设计中的 strata

需要特别注意的是,从 1999-2000 到 2005-2006 周期,部分人口学变量名和后面周期不一致。比如种族变量早期是 RIDRETH1,教育变量早期写法也不同。复现早期文献时,不要直接照搬 2017-2018 周期的变量名。

2.4 复杂抽样设计:为什么必须用权重

NHANES 的调查对象不是简单随机抽样来的,而是按“分层、多阶段、概率抽样”的方式抽取。这意味着不同人群被抽中的概率不同,如果直接对原始数据做均值、回归,结果不能代表全美居民总体。

用一个直觉类比:假设调查时故意多抽了老年人,直接算平均年龄会偏高。权重的作用就是给每个样本一个“放大倍数”,把抽样概率的差异修正回来。R 里做这事最常用的包是 survey,它要求你在分析前声明三个要素:权重、分层(strata)、初级抽样单元(PSU)。

很多新手在第一步读入数据后,直接对列做 t.test 和 lm,然后把结果写进论文。这个做法在审稿人眼里是硬伤,也是 NHANES 复现时结果对不上的最常见原因之一。

3. 原始数据下载:手工点击与 R 脚本批量下载

3.1 官网手工下载

NHANES 数据主页面在 CDC 官网,进入后按周期和模块逐层点击即可下载 XPT 文件。以 2017-2018 周期的人口学文件为例,直接访问的 URL 格式是:

https://wwwn.cdc.gov/Nchs/Nhanes/2017-2018/DEMO_J.XPT

浏览器会直接下载文件。手工下载适合单次少量取文件,但论文复现往往要下五六个文件,而且未来你还会反复换周期,所以建议写脚本。

3.2 用 R 批量下载

下面的脚本会创建 data/raw 目录,并按文件名判断是否已经下载过,避免重复下载。

# 文件路径:scripts/00_download_data.R library(httr) # 以 2017-2018 周期为例:人口学、体测、血压 base_url <- "https://wwwn.cdc.gov/Nchs/Nhanes/" files <- c( "2017-2018/DEMO_J.XPT", "2017-2018/BMX_J.XPT", "2017-2018/BPX_J.XPT" ) dir.create("data/raw", recursive = TRUE, showWarnings = FALSE) for (f in files) { destfile <- file.path("data/raw", basename(f)) if (!file.exists(destfile)) { GET(paste0(base_url, f), write_disk(destfile, overwrite = TRUE)) message("下载完成: ", f) } else { message("本地已存在,跳过: ", f) } }

脚本的关键在于 base_url 和文件路径拼接。只要把 files 列表里的周期和文件名换成目标周期,比如把 "2017-2018/DEMO_J.XPT" 改成 "2015-2016/DEMO_I.XPT",其余逻辑不变。

3.3 用 Python 批量下载

Python 用户可以用 requests 实现同样效果:

# 文件路径:scripts/download_nhanes.py import requests url = "https://wwwn.cdc.gov/Nchs/Nhanes/2017-2018/DEMO_J.XPT" r = requests.get(url, timeout=60) r.raise_for_status() with open("data/raw/DEMO_J.XPT", "wb") as f: f.write(r.content) print("文件大小:", len(r.content), "bytes")

下载完成后,先别急着合并数据。建议先确认文件大小是否正常,很多 XPT 文件在几十 KB 到几 MB 之间,如果下载下来只有几 KB,大概率是请求失败或保存了错误页面。

4. 数据清洗与合并:90% 的问题出在这一步

4.1 读取 XPT 文件

XPT 是 SAS transport 格式,R 里最常用的是 haven 包。

# 文件路径:scripts/01_read_merge.R library(haven) library(dplyr) demo <- read_xpt("data/raw/DEMO_J.XPT") bmx <- read_xpt("data/raw/BMX_J.XPT") bpx <- read_xpt("data/raw/BPX_J.XPT") # 先看列名和数据规模 glimpse(demo)

Python 里用 pandas 读取:

# 文件路径:scripts/load_nhanes.py import pandas as pd demo = pd.read_sas("data/raw/DEMO_J.XPT", format="xport") bmx = pd.read_sas("data/raw/BMX_J.XPT", format="xport") print(demo.shape) print(demo[["SEQN", "RIAGENDR", "RIDAGEYR"]].head())

读取本身不难,真正容易出错的是变量类型。XPT 导入后,性别等类别变量经常是带标签的数值变量,直接用 factor() 转换时要注意底层值是 1、2,不是 "Male"、"Female"。

4.2 筛选变量并合并

下面以“BMI 与收缩压的关联”为例,演示一个最典型的数据流程。先保留人口学关键变量,再按 SEQN 把体测和血压数据合并进来。

# 继续在 scripts/01_read_merge.R 中执行 nhanes <- demo %>% select(SEQN, SDDSRVYR, RIAGENDR, RIDAGEYR, RIDRETH3, DMDEDUC2, INDFMPIR, WTMEC2YR, SDMVPSU, SDMVSTRA) %>% mutate(RIAGENDR = factor(RIAGENDR, levels = c(1, 2), labels = c("Male", "Female"))) %>% inner_join( bmx %>% select(SEQN, BMXBMI, BMXWAIST), by = "SEQN" ) %>% inner_join( bpx %>% select(SEQN, BPXSY1, BPXDI1), by = "SEQN" ) # 只保留 20 岁及以上、关键变量不缺失的人群 nhanes_clean <- nhanes %>% filter(RIDAGEYR >= 20, !is.na(BMXBMI), !is.na(BPXSY1)) saveRDS(nhanes_clean, "data/processed/nhanes_clean.rds")

为什么用 inner_join?因为我们要的是同时具备完整人口学、体测和血压记录的个体。不同模块的样本量本来就不同,比如某些问卷只对特定年龄段提问,直接用 inner_join 会让样本量明显下降。这是正常现象,重点是要能解释清楚样本量的变化原因。

4.3 缺失值处理的基本原则

NHANES 的缺失值通常有以下几种形态:

  • 真正的缺失,变量值为 NA;
  • 不适用,比如只有女性才有某个变量;
  • 拒绝回答或不知道,通常编码为 7、9 等特殊值。

处理原则很简单:先看 Codebook,搞清楚每个特殊值含义,再做删除或归类。不要一上来就用 na.omit 把所有含缺失的行删掉,那样很容易把有效信息一并丢掉。主分析一般做完整案例分析,敏感性分析里再考虑填补或加权。

4.4 多周期合并时权重怎么调整

如果样本量不够,需要合并两个周期,比如 2015-2016 和 2017-2018。这时直接把两个周期的 CSV 拼接并不够,因为权重变量 WTMEC2YR 是针对单个两年周期的。正确做法是:先按 SEQN 合并内部模块,再纵向拼接多周期数据,最后把权重除以周期数。

# 示例:合并两个周期后调整权重 nhanes_two_cycles <- bind_rows(nhanes_2015_2016, nhanes_2017_2018) %>% mutate(WTMEC2YR_ADJ = WTMEC2YR / 2)

合并三个周期就除以 3,四个周期除以 4。不调整权重,相当于把一个周期的总体估计重复计算,标准误和置信区间都会失真。

5. 统计分析:把 survey 设计对象放在第一位

5.1 构建复杂抽样设计

使用 survey 包时,不要直接用数据框做回归,而是先声明调查设计对象。

# 文件路径:scripts/02_analysis.R library(survey) nhanes_clean <- readRDS("data/processed/nhanes_clean.rds") nhanes_svy <- svydesign( ids = ~SDMVPSU, strata = ~SDMVSTRA, weights = ~WTMEC2YR, nest = TRUE, data = nhanes_clean )

这段代码里,ids 对应初级抽样单元 PSU,strata 对应分层变量,weights 使用体检权重 WTMEC2YR。nest = TRUE 是 NHANES 分析的标准设置,因为 PSU 编号在层内是嵌套的。

5.2 加权描述统计

有了设计对象,描述统计就全部基于权重计算。

# 加权均值 svymean(~BMXBMI, nhanes_svy, na.rm = TRUE) # 分性别加权均值 svyby(~BMXBMI, ~RIAGENDR, nhanes_svy, svymean, na.rm = TRUE) # 加权分位数 svyquantile(~BMXBMI, nhanes_svy, quantiles = c(0.25, 0.5, 0.75), na.rm = TRUE)

如果要做论文里的 Table 1,可以用 tableone 包的 svyCreateTableOne,它能直接接收 survey 设计对象,按分组变量输出加权基线特征。

library(tableone) tab1 <- svyCreateTableOne( vars = c("RIDAGEYR", "BMXBMI", "BMXWAIST", "BPXSY1"), strata = "RIAGENDR", data = nhanes_svy ) print(tab1, showAllLevels = TRUE)

5.3 加权回归

关联分析用 svyglm,它的用法和 glm 几乎一样,只是 design 参数传入的是调查设计对象。

# 收缩压对 BMI、年龄、性别的加权线性回归 fit <- svyglm(BPXSY1 ~ BMXBMI + RIDAGEYR + RIAGENDR, design = nhanes_svy) summary(fit)

如果结局是二分类变量,比如高血压,把 family 改成 quasibinomial 即可,输出的是加权 OR。

这里真正容易踩坑的是亚组分析。不要先 filter 数据再重新构建设计对象,更推荐的做法是在设计对象内部用 subset(),这样能保留正确的方差估计逻辑。

# 45 岁以上亚组 fit_older <- svyglm( BPXSY1 ~ BMXBMI + RIDAGEYR + RIAGENDR, design = subset(nhanes_svy, RIDAGEYR >= 45) ) summary(fit_older)

5.4 结果解释的关键细节

svyglm 输出的 beta、SE、p 值都能直接用于论文。但有一个细节常被忽略:分类变量的参考组。R 默认按字母序或因子水平序取参考组,如果用 factor 明确指定了水平和标签,最好在结果表里再确认一次参考组,避免“男性 vs 女性”的方向写反。

6. 结果绘图:从分布图到森林图

6.1 加权分布图

ggplot2 本身不支持复杂抽样权重,但我们可以把权重映射到 aes(weight=) 上,画近似加权分布。

# 文件路径:scripts/03_plot.R library(ggplot2) ggplot(nhanes_clean, aes(x = BMXBMI, weight = WTMEC2YR)) + geom_histogram(binwidth = 2, fill = "#4e79a7", color = "white") + labs(x = "BMI (kg/m^2)", y = "加权人数") + theme_minimal(base_size = 14) ggsave("output/figures/bmi_dist.png", width = 7, height = 5, dpi = 300)

6.2 森林图

森林图是 NHANES 论文中出现频率最高的图之一,适合展示亚组分析或敏感性分析结果。下面代码里的 OR 数值只是演示数据,用于说明画法,不是真实统计结果。

# 文件路径:继续在 scripts/03_plot.R 中执行 forest_data <- data.frame( subgroup = c("总体", "男性", "女性", "18-44岁", "45-64岁", "65岁及以上"), OR = c(1.32, 1.41, 1.25, 1.18, 1.36, 1.48), low = c(1.18, 1.22, 1.06, 1.03, 1.19, 1.30), high = c(1.47, 1.64, 1.48, 1.35, 1.55, 1.70) ) forest_data$subgroup <- factor( forest_data$subgroup, levels = rev(forest_data$subgroup) ) ggplot(forest_data, aes(x = OR, y = subgroup)) + geom_point(size = 3, color = "#2c3e50") + geom_errorbarh(aes(xmin = low, xmax = high), height = 0.2, color = "#2c3e50") + geom_vline(xintercept = 1, linetype = "dashed", color = "grey40") + labs(x = "OR (95% CI)", y = NULL, title = "BMI 与收缩压关联的亚组森林图(演示数据)") + theme_minimal(base_size = 13) ggsave("output/figures/forest_demo.png", width = 7, height = 4.5, dpi = 300)

实际使用时,OR 和置信区间应来自上一章的 svyglm 结果。可以从多个模型中提取系数和置信区间,构造成 forest_data 这样的数据框,再套用绘图代码。

6.3 图片导出格式

期刊一般要求 300 dpi 以上的 TIFF 或 PNG,字体建议保持 10-12pt。ggsave 里设置 dpi = 300 即可。中文期刊可能会要求宋体或 Times New Roman,字体问题可以在投稿前统一处理,不必在分析阶段花太多时间。

7. 论文复现:把一篇已发表文章拆成可执行任务

7.1 从原文提取信息

拿到一篇 NHANES 论文后,先别急着写代码,先把方法部分翻译成下面这张清单:

  • 数据来源:哪个周期,是否合并多周期;
  • 纳入人群:年龄范围、性别限制、排除条件;
  • 暴露变量:来自哪个模块,如何定义;
  • 结局变量:来自哪个模块,如何定义;
  • 协变量:是否调整年龄、性别、种族、收入、教育、BMI;
  • 统计方法:加权回归、亚组分析、敏感性分析。

以“BMI 与收缩压关联”为例,方法部分通常写成“数据来自 NHANES 2017-2018,纳入 20 岁及以上成年人,排除孕妇及血压数据缺失者。收缩压取三次测量平均值,BMI 由实测身高体重计算。”

代码层面要做的事就变成了:下载 DEMO、BMX、BPX 三个文件,按 SEQN 合并,年龄过滤,定义 BMI 和平均收缩压,做加权线性回归。

7.2 复现顺序

推荐按这个顺序执行:

  1. 先复现样本量。把排除条件逐步加上,每一步都看剩余人数,和原文流程图对比;
  2. 再复现 Table 1。用 svyCreateTableOne 输出基线表,和原文表格逐列对比;
  3. 然后复现主分析。先不加协变量,再加协变量,观察系数变化是否和原文一致;
  4. 最后复现亚组和森林图。用 subset() 构建亚组模型,把 OR 和置信区间填进绘图数据框。

每完成一步,就把“样本量、系数、置信区间”记到 docs 目录下的复现记录里。这样做的好处是,结果对不上时能快速定位是清洗问题、变量定义问题还是统计方法问题。

7.3 复现结果对不上的常见原因

即便流程完全相同,复现结果也可能和原文有微小差异。常见原因有三个:

  • 变量定义不同,比如收缩压用第一次测量还是三次平均;
  • 缺失处理不同,比如是否排除了服用降压药的人;
  • 数据版本更新,比如 CDC 后续修正过某些实验室变量。

所以复现的目标不是追求小数点完全一致,而是确认方向、效应量和显著性判断一致。只要逻辑一致,差异在合理范围内,就算复现成功。

8. 常见问题与排查思路

问题现象可能原因排查方式解决方案
读取 XPT 报错或读出来乱码文件下载不完整或格式判断错误检查文件大小,R 里用 haven::read_xpt,Python 里确认 format="xport"重新下载,并核对 URL 是否正确
找不到某个变量变量只存在于特定周期,或文件名写错打开对应周期 Codebook,搜索变量名换用该周期的等价变量,或调整数据周期
inner_join 后样本量骤减不同模块覆盖人群不同,或 key 变量不一致分别查看每个文件的样本量和缺失情况用 left_join 保留主数据,再明确缺失处理规则
svydesign 报错PSU 或 strata 列被删除,或数据被提前 filter检查数据是否包含 SDMVPSU、SDMVSTRA从完整 DEMO 文件重新取回抽样变量
svyglm 出现收敛警告亚组样本量太小或权重极端查看亚组人数和权重分布合并周期扩大样本量,或改用更稳健的模型
权重计算后发现结果特别大多周期合并后没有调整权重检查权重变量是否除以周期数构造 WTMEC2YR_ADJ = WTMEC2YR / 周期数
复现结果和原文不一致变量定义或缺失处理不同仔细对照原文 Methods 和 Codebook调整定义后重跑,确认差异原因

9. 最佳实践与工程建议

9.1 固定项目目录结构

NHANES 项目涉及文件多、脚本多、变量多,建议每个项目都按统一目录组织。

nhanes_paper/ ├── data/ │ ├── raw/ # 原始 XPT,不修改 │ └── processed/ # 清洗后的 RDS / CSV ├── scripts/ │ ├── 00_download.R │ ├── 01_read_merge.R │ ├── 02_analysis.R │ └── 03_plot.R ├── output/ │ ├── tables/ │ └── figures/ └── docs/ └── variable_notes.md

脚本用数字编号,保证执行顺序一目了然。原始 XPT 永远不手工改动,每次分析都从清洗脚本重新生成处理后的数据。

9.2 先写变量说明,再写代码

下载文件前,先在 docs/variable_notes.md 里列一张表,记录每个变量的文件来源、周期、标签和编码含义。这一步看起来慢,实际能节省大量返工时间。尤其是隔了一周再回来看代码时,变量说明比任何注释都管用。

9.3 保存中间结果但保留复现能力

saveRDS 保存清洗后的数据可以加快调试速度,但论文投稿前一定要保证能从原始 XPT 一键跑到最终结果。不要出现“手工改了一个 Excel 再继续分析”的情况,否则别人无法复现,自己也无法审计。

9.4 注意 NHANES 数据使用要求

NHANES 是公共数据,分析不需要医院伦理审批,但发表论文时需要在方法部分写明数据来源、调查设计和加权方法,并在致谢或声明中提及 NCHS。建议在官网查阅最新的数据使用说明,不同期刊也可能有具体要求。二次发表或数据再分析时,务必遵循原文和数据库的许可边界。

9.5 常见统计分析规范

  • 连续变量用加权均值和 95% 置信区间;
  • 分类变量用加权百分比和例数;
  • 多周期合并要调整权重;
  • 亚组分析使用 subset(design) 而不是提前过滤后重建设计;
  • 模型调整的协变量要在方法部分逐一说明。

10. 总结与后续学习方向

这篇教程把 NHANES 数据库实战拆成了五步:下载、清洗、加权分析、绘图、论文复现。真正核心的判断只有一条:NHANES 分析难在流程标准化,而不是统计理论。只要固定了“文件命名规则—关键变量—survey 设计—输出格式”这条链路,后面的工作就是重复执行和验证。

下一步你可以做三件事。第一,选一个你熟悉的临床问题,比如“睡眠时长与高血压”“BMI 与糖化血红蛋白”,找一篇已发表的 NHANES 论文,按第 7 章的清单复现一遍。第二,把清洗和绘图代码整理成自己的模板,以后换周期、换变量时只改配置不改逻辑。第三,把单一周期扩展到两周期合并,学习权重调整和更复杂的亚组分析,这一层能力是 NHANES 进阶研究中区分新手和熟练者的关键。

最后提醒一句:复现别人的论文只是学习手段,真正做新研究时,一定要从临床问题出发设计暴露、结局和协变量,而不是先下载数据再硬凑关联。NHANES 数据量足够大、变量足够多,但这恰恰意味着分析过程必须透明、规范,每一步筛选和定义都要经得起审稿人追问。把一套标准流程练熟,比囤积再多教程都有用。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询