做临床和公卫研究的人,十有八九都绕不开 NHANES 这个数据库。它是美国国家健康与营养调查的英文缩写,从 1999 年开始每两年发布一个周期的数据,公开免费,变量覆盖人口学、饮食、体检、实验室检查、慢性病问卷等几百个维度。很多新手第一次上手,最容易卡在两个地方:一是数据合并,二是加权分析。前者是因为数据文件太分散,不吃透合并逻辑就会得到一张行数爆炸或变量对不上的表;后者是因为 NHANES 是复杂抽样设计,不用权重直接算,结果不能代表美国全国人群,发文章时也容易被审稿人揪住不放。
这篇文章就把我摸爬滚打多年总结出来的数据合并与加权分析完整路径写下来。不管你是用 R 还是 SAS,只要理解了这里的核心逻辑,拿到任何一期 NHANES 数据都能快速上手,得到能发表、能站得住脚的统计结果。适合刚开始接触 NHANES 的硕博生、做临床回顾性研究的医生,以及想用公开数据库发文的科研人员。
1. 先搞懂 NHANES 的数据结构,合并才有意义
1.1 一个周期一份数据,每个周期又分几十张表
NHANES 的数据不是一个大表格,而是按调查周期和模块拆分的。每两年算一个周期,比如 1999-2000、2001-2002、一直到 2017-2018、2021-2023。每个周期下面,又按主题分成几十个独立的数据文件(XPT 格式,SAS 传输文件)。常见的有:
- 人口学数据:DEMO
- 糖尿病问卷:DIQ
- 血压问卷:BPQ
- 医疗状况问卷:MCQ
- 吸烟烟草使用问卷:SMQ
- 体重史问卷:WHQ
- 实验室检测数据:GLU(空腹血糖)、INS(胰岛素)、TCHOL(总胆固醇)等
- 体检数据:BPX(血压测量)、BMX(身体测量)
问题就出在这里。你想研究"糖尿病患病率跟肥胖的关系",很可能需要来自 DEMO 的年龄性别、来自 DIQ 的是否确诊糖尿病、来自 BMX 的 BMI、来自 GLU 的空腹血糖。四份文件各有各的样本量、各有各的缺失模式,不合并就根本没法做多变量分析。
1.2 合并和加权为什么是绕不开的两件事
先说一个我见过很多次的场景:有人下载了 DEMO 和 DIQ,用左联结一拼,然后直接算患病率,得到一个数字,觉得大功告成。但等到论文审稿时,审稿人问了一句:"你的分析是否考虑了 NHANES 的抽样权重和复杂设计?" 整个分析就得推倒重来。
如果你稍微了解一点 NHANES 的抽样机制,就会明白这不是审稿人苛刻,而是方法论的基本要求。NHANES 采用的是分层多阶段概率抽样,不是简单随机抽样。调查团队先在全国范围内抽县,再从县里抽街区、住户,最后抽人。这个过程中,不同人群被抽中的概率完全不一样:老人、未成年人、黑人、墨西哥裔美国人等亚群体会被人为过采样,以保证亚组估计的稳定性。
这样一来,每个人其实"代表"了美国总人口中的一定数量。比如一个 70 岁的白人男性被抽中,他可能代表全国 3500 个类似的人;而一个中年人被抽中,可能只代表 800 个人。这个"代表多少人"的数量就记录在权重变量里。加权分析的本质,就是把每个个体还原成它代表的一群人,再进行统计推断。
所以,如果你只做合并不做加权,你的结果只是"样本特征"而不是"全国人群特征";如果你只用权重但忽略分层和整群信息,你的标准误会严重偏小,假阳性风险直线上升。这两件事,本质上是在解决同一个问题的两面:如何让样本说话,替全国人口代言。
2. 数据合并:从入门到熟练的完整路径
2.1 横向合并:用 SEQN 把不同数据表拼起来
NHANES 每一份数据文件里都有一个唯一的受访者编号,叫 SEQN。这是横向合并的钥匙,相当于关系型数据库里的主键。你要把人口学数据、问卷数据、实验室数据拼到同一行,只能靠它。
我用 R 做横向合并的常规姿势是 dplyr 的left_join()。假设你已经把 DEMO 和 DIQ 都读进来了:
library(dplyr) demo <- read_xpt("DEMO.XPT") # 实际下载后解压 diq <- read_xpt("DIQ.XPT") merged <- demo %>% left_join(diq, by = "SEQN")这里的关键是理解left_join的方向。它以左边表(DEMO)为基准,右边表(DIQ)只负责把匹配到的变量加过来。DEMO 是所有接受家庭访谈的人,DIQ 理论上应该覆盖同一批人,所以直接用 DEMO 做左表通常不会丢样本。
如果要做多张表,可以链式加下去:
merged <- demo %>% left_join(diq, by = "SEQN") %>% left_join(bmx, by = "SEQN") %>% left_join(glu, by = "SEQN")每次合并后,我建议马上看一眼nrow(merged)是否等于 DEMO 的行数,而不是莫名变多或变少。
2.2 纵向合并:把多个周期的数据摞起来
单一年度周期的样本量有时候不够用,尤其做亚组分析或者研究罕见病时。这时候需要把多个周期的数据纵向堆叠起来,也就是按行拼接。以 R 为例:
df_9910 <- read_xpt("DEMO_1999_2000.XPT") df_0102 <- read_xpt("DEMO_2001_2002.XPT") pooled <- bind_rows(df_9910, df_0102)纵向合并最大的坑在于变量名和编码跨周期可能不一致。比如某一期叫DMDEDUC,另一期叫DMDEDUC2;或者同一个变量的取值编码在两个周期里定义不同。我自己的习惯是:先names()对比两个数据框,把新增变量、缺失变量逐个找出来,必要时用rename()统一变量名,用case_when()统一编码。
建议在纵向合并后加一列周期标识,方便后续分析时按周期分层或做敏感性分析:
df_9910$cycle <- "1999-2000" df_0102$cycle <- "2001-2002" pooled <- bind_rows(df_9910, df_0102)2.3 合并时最容易踩的 3 个坑
第一个坑是类型不匹配。SEQN 在绝大多数文件里是数值型,但有些文件读进来可能显示为字符型。left_join一旦发现两个表的 key 类型不一致,轻则报错,重则悄悄匹配不上产生大量缺失。我建议合并前统一做一次as.numeric(SEQN),或者先用str()检查类型。
第二个坑是 ID 不唯一。正常情况下 SEQN 在同一文件里是唯一的,但如果你合并前做过数据清洗、筛选或者其他操作,导致同一个 SEQN 出现多次,合并时就会产生笛卡尔积,行数暴增。我见过有人把行数从 9000 莫名变成 3 万,就是因为数据集被重复拼接了。检查方法很简单:sum(duplicated(df$SEQN)),如果大于 0,先处理重复问题。
第三个坑是变量名冲突。多张表合并时,如果两表都有同名但含义不同的列,left_join会自动加后缀.x、.y,这倒不影响数据,但容易让人搞混。建议合并前用select()只保留需要用的变量,既能避免冲突,又能大幅减少内存占用。
注意:合并之前,永远先确认主键的唯一性和类型。这是我在 NHANES 实战里反复强调的第一条原则。
2.4 大文件合并的提速思路
NHANES 单周期的 DEMO 文件只有一万行左右,对现代计算机来说不算大。但如果你把几十个周期的数据全部下载下来,从几百个 XPT 里挑变量,再用 R 合并,还是会遇到内存不够、运行缓慢的问题。
我常用的组合拳是配合data.table包来提速。data.table的合并语法很简洁,而且底层做了并行化和内存优化:
library(data.table) demo <- as.data.table(read_xpt("DEMO.XPT")) diq <- as.data.table(read_xpt("DIQ.XPT")) merged <- demo[diq, on = "SEQN"] # 右连接;如果需要左连接写成 demo[diq, on="SEQN", nomatch=0] 之外的方式另外一个更重要也更容易被忽略的策略是:先筛选再合并。如果你研究糖尿病,根本不需要把 DEMO 里几百个变量全部带上,只需要年龄、性别、种族、教育程度这几个协变量。先select()掉无关变量,再去合并,内存压力会小非常多。不要偷懒把整张表合完再选变量,这是内存杀手。
3. 加权分析:让结论真正代表全国人口
3.1 NHANES 抽样设计决定了你不能"裸奔"
很多人拿到数据后第一反应是:直接用table()数人数、算百分比。这在描述样本特征时可以做,但是只要你打算把结论推广到全美国人群,就必须给每个观测赋予权重。
NHANES 的抽样结构本质上是一个三层模型:第一层是分层(strata),第二层是初级抽样单位(PSU,通常是县或街区),第三层是受访者个体。为了简化使用,官方在数据文件里提供了一系列"掩蔽"变量:
SDMVSTRA:掩蔽后的分层变量SDMVPSU:掩蔽后的初级抽样单位变量WTINT2YR:访谈权重,2 年周期WTMEC2YR:体检(MEC)权重,2 年周期
这组变量在 DEMO 文件中都能找到。它们的存在意味着,在 R 里做分析时要创造一个"调查设计对象",把分层、整群、权重三个信息全部带进去。只带权重不带分层和整群信息,等于把复杂抽样当成了简单随机抽样,标准误必然被低估。
3.2 权重怎么选:WTINT2YR 还是 WTMEC2YR
权重选择的核心原则是:你用的变量来自哪个环节,就用哪个环节的权重。
NHANES 的调查流程大致是:先是家庭访谈,收集问卷变量;然后邀请受访者到移动体检中心(MEC)做体检和实验室检测。不是所有人都愿意去 MEC,所以:
- 如果你只用问卷变量(比如自报糖尿病、吸烟、收入),用
WTINT2YR - 只要你用了任何体检或实验室检测变量(比如血糖、BMI、血压),用
WTMEC2YR
因为只有参加了 MEC 检查的人才有实验室数据,如果你用WTMEC2YR去分析问卷变量,样本量会稍稍变小,但更符合"实际测量"人群的代表性。反过来,如果你用WTINT2YR去分析实验室指标,这个权重无法校正体检人群的选择偏差,结果是有问题的。
多周期合并时权重还需要进一步调整。NHANES 两年的权重是基于两年抽样设计计算的,如果合并了 4 年(两个周期),理论上样本量翻倍,每个个体的权重应该减半。通用的做法是:
合并 2 个周期:最终权重 = 原周期权重 / 2 合并 3 个周期:最终权重 = 原周期权重 / 3在实操中,如果两个周期的样本量大致相当,直接对每个周期内部的WTMEC2YR除以周期数(例如合并 1999-2002 就把 1999-2000 的权重和 2001-2002 的权重都除以 2)即可。
3.3 R 代码实战:加权均值和加权患病率
下面给出一段完整的、可以直接改的 R 代码,演示如何用survey包做加权分析。假设我们已经完成合并,得到了一个包含SEQN、RIAGENDR、RIDAGEYR、DID040(是否确诊糖尿病)、WTMEC2YR、SDMVPSU、SDMVSTRA的数据框merged。
library(survey) # 1. 建立复杂抽样设计对象 nhanes_design <- svydesign( id = ~SDMVPSU, # 初级抽样单位 strata = ~SDMVSTRA, # 分层变量 weights = ~WTMEC2YR, # 权重 data = merged, nest = TRUE # 掩蔽变量是嵌套设计,务必设为 TRUE ) # 2. 加权估计糖尿病患病率(DID040 假设 1=是,2=否) merged$diabetes <- ifelse(merged$DID040 == 1, 1, 0) svymean(~diabetes, nhanes_design, na.rm = TRUE) # 3. 按性别分组计算患病率 svyby(~diabetes, ~RIAGENDR, nhanes_design, svymean, na.rm = TRUE) # 4. 加权线性回归:血糖与年龄的关系 merged$glucose <- merged$LBXGLU # 假设来自实验室血糖文件 model <- svyglm(glucose ~ RIDAGEYR + RIAGENDR, design = nhanes_design) summary(model)svydesign()里的nest = TRUE是一个容易被忽略但很重要的参数。因为 NHANES 官方提供的SDMVPSU和SDMVSTRA是掩蔽处理过的变量,且 PSU 编号在层内嵌套,所以必须显式指定nest = TRUE。漏掉这个参数,方差估计会出问题,哪怕结果看起来正常。
svymean()输出里会有mean和SE两列。比如糖尿病患病率估计为 0.103,SE 为 0.008,意思是在全国人群中估计患病率为 10.3%,95% 置信区间大致是 10.3% ± 1.96 × 0.8%。这比不带权重的朴素计算更可信。
3.4 亚组分析时权重的处理
做亚组分析(比如只研究 60 岁以上老年人)时,最常见的问题是要不要把权重重新计算一遍。我的经验是:不要手动重新计算,直接在设计的子集上分析即可。
正确做法是用subset()对调查设计对象做子集化:
elderly_design <- subset(nhanes_design, RIDAGEYR >= 60) svymean(~diabetes, elderly_design, na.rm = TRUE)subset()会保留原来的 PSU、分层和权重信息,只是把子集外的观测去掉,方差估计时依然能正确反映原始抽样结构。如果你直接用filter()先把数据框子集化,再新建svydesign对象,会让你丢失原始设计信息,标准误可能偏小。
不过有一个微妙点:亚组分析时,权重并不会重新校准。如果某个亚组在抽样时是过采样的,其权重总和与总人群的比例可以相差很多。NHANES 官方建议大部分时候直接使用原始权重,因为权重在构造时已经考虑了多阶段抽样的特征。但对于某些特殊亚组(如单独分析某一族裔),是否重新归一化权重是个学术争论点。作为实战派,我的建议是:主流分析直接使用原始权重,如果你想做敏感性分析,可以再试一下把权重放缩到亚组样本量相当的尺度,看结论是否稳健。
3.5 多周期合并后的权重调整
多周期合并是 NHANES 实操中非常高频的操作,因为很多罕见疾病或特殊年龄段,单周期样本量根本不够。前面提到权重要除以周期数,这里举一个具体例子。
假设你把 2015-2016 和 2017-2018 两个周期的 DEMO 数据合并。2015-2016 周期的每个个体权重是WTMEC2YR,2017-2018 周期同样。合并后如果想要代表 4 年时期的人群平均健康水平,正确的权重变量应该是:
merged <- merged %>% mutate( WTMEC4YR = ifelse(cycle == "2015-2016", WTMEC2YR / 2, WTMEC2YR / 2) )你会发现两个周期都是除以 2,因为从 2 年权重变成 4 年权重,总人数翻倍,权重按比例缩小。如果合并 6 年,就除以 3,以此类推。
但也有例外,比如 NHANES 1999-2000 周期非常特殊。有些资料中,它的体重变量名和权重变量名跟后面的周期不完全一致,实际下载后你会看到类似WTMEC4YR这样的命名。所以每次下载新数据,我都强烈建议打开官方代码表(Codebook)查看权重变量的准确名称和说明,再统一重命名,不要凭经验硬套。
4. 常见问题与排查技巧实录
4.1 合并后样本量比预期少了一大截
这通常是合并方向选错了。比如你用的是inner_join()或full_join(),但两张表的覆盖范围本来就不一样:实验室检测只覆盖了参加 MEC 检查的一部分人,如果你以实验室表为主表去合并 DEMO,行数自然少于 DEMO 的总人数。
解决办法:分清分析目标。如果你是分析实验室指标,以实验室文件为"事实表"没问题;如果你是分析人口学特征,就应以 DEMO 为主表左连接其他表。凡是遇到样本量意外减少,先检查合并的方向和 key。
4.2 加权后患病率和未加权结果差异巨大
这不一定是代码 bug,反而可能是正常现象。NHANES 过采样了老人、未成年人和少数族裔,未加权样本中这些人群的比例跟实际全国人口差异很大。加权就是把这些偏差拉回来。
如果加权前后差异大到离谱(比如患病率从 15% 变成 40%),我建议按顺序排查:权重变量选错了?没有按周期数调整?某些极端权重值有没有被错误保留?可以用summary(weights)或svymean(~weights)检查权重分布,正常的 2 年权重通常在几百到几十万之间,出现上亿的值要警惕。
4.3 标准误为什么特别小
标准误过小,十有八九是nest = TRUE没设置,或者 PSU/分层变量没有正确纳入。简单随机抽样的方差比复杂抽样的方差往往更小,因为复杂抽样的整群效应会增大方差。用svydesign()时,务必检查代码里是否包含id = ~SDMVPSU、strata = ~SDMVSTRA和nest = TRUE。
4.4 权重变量找不到或者变量名对不上
NHANES 不同周期的变量命名确实存在差异,尤其是 1999-2000 周期。我的经验是:打开每一年度的 DEMO 代码表,直接搜索"weight"关键词,把实际存在的权重变量名列出来。不要想当然地用WTMEC2YR一套到底。
4.5 多周期合并后某些周期变量完全缺失
不同周期的问卷问题有过增减。比如有些心理健康问卷只在特定年份调查过,或者同一个概念换过问题编号。这时候有两种处理方式:一是做可用数据的分层分析,在论文方法里明确说明"该指标仅在 XX 周期收集";二是放弃该变量,选一个在所有周期都存在的替代变量。千万别逞强用一个变量强行合并多个周期,最后发现全是缺失值。
我把常见问题整理成一个速查表,方便你自查:
| 问题现象 | 大概率原因 | 排查动作 |
|---|---|---|
| 合并后行数暴增 | key 不唯一,导致笛卡尔积 | duplicated()检查 ID |
| 合并后变量大量缺失 | 类型不匹配,没匹配上 | 检查 key 的class() |
| 加权前后差异巨大 | 权重给错使用者 | 核对 WTINT2YR 与 WTMEC2YR |
| 标准误特别小 | 忽略 PSU/strata 或nest=FALSE | 检查svydesign()参数 |
| 多周期权重未变 | 忘记按周期数调整 | 原权重除以周期数 |
5. 完整实操案例:糖尿病患病率的加权估计
下面我拿一个实际场景完整走一遍流程。假设研究问题是:2017-2018 周期美国成年人中,不同性别、不同年龄段的糖尿病患病率是多少?整个流程分成数据读取、合并、设计加权、估计四步。
第一步,读取所需的 DEMO 和 DIQ 两份文件,并保留关键变量:
library(haven) library(dplyr) library(survey) demo <- read_xpt("DEMO.XPT") %>% select(SEQN, RIAGENDR, RIDAGEYR, SDMVPSU, SDMVSTRA, WTMEC2YR) diq <- read_xpt("DIQ.XPT") %>% select(SEQN, DID040)第二步,合并并清洗变量:
merged <- demo %>% left_join(diq, by = "SEQN") %>% filter(RIDAGEYR >= 20) %>% mutate( diabetes = ifelse(DID040 == 1, 1, 0), age_group = cut(RIDAGEYR, breaks = c(20, 40, 60, 80), labels = c("20-39", "40-59", "60-79")) )第三步,构建加权设计:
nhanes_design <- svydesign( id = ~SDMVPSU, strata = ~SDMVSTRA, weights = ~WTMEC2YR, data = merged, nest = TRUE )第四步,输出结果:
svyby(~diabetes, ~RIAGENDR, nhanes_design, svymean, na.rm = TRUE) svyby(~diabetes, ~age_group, nhanes_design, svymean, na.rm = TRUE)这里得到的每个估计值都自带了标准误和置信区间,你可以直接写进论文的 Table 1 或者结果部分,审稿人没法挑你方法上的刺。
有一点值得留意:filter(RIDAGEYR >= 20)是在构建调查设计之前做的。如果在设计之后再子集化,用subset()更严谨。实际应用中,如果你已经建立好了nhanes_design,再需要限制年龄,我更建议写成:
nhanes_design_adults <- subset(nhanes_design, RIDAGEYR >= 20)这样做可以确保后续所有估计都在同一个设计对象里操作。
6. 我的个人经验和最终建议
踩过这么多次坑之后,我慢慢形成一个习惯:拿到任何一期 NHANES 数据,我会先花十分钟做三件事。一是打开变量代码表,把 SEQN、权重变量、PSU、分层变量的名字和说明抄下来;二是确认我需要的几个核心变量在当前周期都存在,编码方式跟预期一致;三是先做一次小样本的探索性合并,打印出行数和前几行,确认逻辑没问题再跑全量。
这个习惯帮我避免了很多后期返工。做数据合并时,永远记住那句老话:先看结构,再写代码。做加权分析时,永远记住:权重解决"代表性"问题,分层和整群解决"方差"问题,两个都别丢。
NHANES 这个数据库的数据是公开的、免费的重磅公共资源,但它的门槛不在于数据获取,而在于数据处理的方法是否正确。数据合并和加权分析是两道基本功,练好了,从 1999 年到最新的每一期数据都能拿来做分析,无论是发文章还是做公卫评估,都会顺手很多。最后再分享一个小技巧:每次合并完数据,随口问自己一句"如果别人拿到我的 R 脚本,能复现出同样的表吗?",当你养成这种习惯,你的数据分析流程就会越来越严谨。