简介:这是一份面向临床科研人员与R语言使用者的NHANES数据清洗代码包,针对美国国家健康与营养调查数据量大、格式复杂、缺失值多等痛点,系统提供从数据选择、合并、清洗、缺失插补到协变量筛选的完整可复现流程。包内共19个文件,包括9个CSV数据文件、2个R脚本、2个Shell运行脚本,以及Markdown说明文档、HTML报告、PNG分布图等辅助内容,压缩包大小约149.99MB,既便于对照学习,也可直接复用。目前已有442人学习,适合正在处理NHANES数据、希望获得规范清洗思路的中级数据分析用户。借助该代码包,可重点掌握tidyverse与haven的数据合并技巧,运用mice包完成多重插补,学会剔除无效值、筛选协变量并规范化列名,最终生成干净整洁、可直接用于统计建模的数据集,大幅提升分析效率。 我第一次跑NHANES数据的时候,血压那一列的888和777就差点把我忽悠瘸了。当时我以为是极端高血压病人,差点把这个变量当异常值整列清掉,直到翻到数据字典才发现,这些三位数对应的是“拒绝测量”、“检测失败”之类的特殊缺失标记。这件事之后我养成了一个习惯:任何NHANES文件读进来,第一件事不是跑描述统计,而是先对照Codebook把缺失编码理清楚。
这篇指南就是围绕这个习惯展开的。我会以一个真实周期(2017-2018,也就是带_J后缀的那一批)为例,讲清楚NHANES数据结构、缺失值编码、多文件合并、权重保留和重复周期拼接这些事,并且在最后给出一套可以直接跑的pandas清洗流水线代码。适合刚接触NHANES的医学研究生、公共卫生从业者,以及一切被XPT文件和777/999逼疯过的科研人。
1. 两年一个周期的文件迷宫,SEQN才是唯一钥匙
1.1 NHANES的数据结构,比想象中更“碎”
NHANES不是一张大宽表,它是按两年一个周期发布的,每个周期下面又有几十个按检查模块拆分的独立数据文件。比如人口学信息在DEMO_*.XPT里,血压检查结果在BPX_*.XPT里,饮食摄入在DR1TOT_*和DR2TOT_*里,实验室检测又拆成血清、全血、尿液等多个文件。
这个设计对于CDC来说是合理的,因为不同检查项目覆盖的人群不一样——有的是全年龄段,有的只针对2岁以上,有的只检查20岁以上,根本没办法塞进同一张表。但对于我们做清洗的人来说,这就意味着很多字段的缺失其实不是“数据没采集”,而是“这个人压根不在这个检查子样本里”。这两种缺失在分析时的处理方式完全不同,后面会细说。
各文件之间靠SEQN这个受访者ID进行横向拼接,它在一个周期内是唯一的。整个NHANES清洗的第一原则就是:任何分析数据集,主键都是SEQN。如果你发现合并之后SEQN有重复,或者某个表里SEQN出现多次,先别急着去重,搞清楚这个文件是不是存在重复测量记录再说。
1.2 缺失值不是pandas默认的那套NaN
NHANES的缺失编码体系非常复杂,而且不同周期、不同变量之间并不完全一致。常见的大致有几类:
- 数值型大编码,比如
777表示“拒绝回答”,999表示“不知道”,888表示“无法检测或无法确定”; - 小数编码,比如
.A、.B这类SAS特殊缺失,pandas读取时往往会转成NaN; - 字符型的空值和
b'',多见于字符变量; - 真正的数值缺失,比如没做某项检查,该字段就是空。
麻烦的地方在于,pandas的read_sas在读取XPT文件时,对某些缺失编码会直接转成NaN,对另一些却保留成777这种“看似正常的数值”。所以你不能假设读进来就已经清洗好了,数据字典才是唯一的裁判。
2. 动手清洗前,先花十分钟读懂命名与字典
2.1 变量名后缀和周期编号的对应关系
NHANES文件名里的后缀字母是周期编号,不是乱标的。_A对应1999-2000周期,_B对应2001-2002,依此类推,到_J就是2017-2018。这个规律对跨周期合并很重要,因为不同周期里同一个变量的名称也可能带上不同后缀,比如某些问卷变量在旧周期叫DPQ010,在新周期可能叫DPQ010后面跟大写字母。
更准确地说,同一个“逻辑变量”在不同周期的文件里经常名字完全一样,但变量的含义、选项编码甚至取值范围会微调。这也是为什么我强烈建议:不要闭着眼睛横向拼接不同周期的同名变量,先打开对应周期的Codebook逐列核对一遍。
2.2 数据字典里必看的三列
我每次清洗前会先下载对应周期的数据字典,也就是那个HTML或PDF格式的Codebook。不用从头看到尾,重点看三块:
- Variable Description:这个变量的判断对象是谁,适用年龄段是多少。这里能直接解释掉一大批结构性缺失。
- Code or Value:每个具体取值对应的含义,特别是
777、999这类特殊值的定义。 - SAS Label:有些变量的Label会把子样本范围说清楚,比如“Males aged 2-5 years”,这会直接影响你是否该把这个变量放进全样本分析。
这一步做完,你对整个文件的结构性缺失大概是心里有数的。后面写清洗代码的时候,缺失值字典就能直接从Codebook里抄过来。
2.3 用pandas读XPT而不是转CSV
很多人习惯先把XPT转成CSV再用Excel看,我不太推荐。原因有两个:一是XPT里有些SAS特殊缺失值在转CSV的过程中会被悄悄抹掉,等你读入pandas时已经丢了信息;二是直接用pd.read_sas读XPT非常方便,读取时把format='xport'指对就行。
import pandas as pd demo = pd.read_sas('DEMO_J.XPT', format='xport') bpx = pd.read_sas('BPX_J.XPT', format='xport')读进来的DataFrame列名全部是大写,属于正常现象。有的版本里字符列会读成字节串,比如显示成b'Some Text',这时候用df[col] = df[col].str.decode('utf-8')处理一下即可。
3. 合并数据的正确姿势:内连接为主,但别忽略重复行
3.1 把人口学变量和检查变量拼到一起
同周期内的多文件合并,核心就是pd.merge,主键用SEQN,连接方式多数情况用how='inner'或者how='left',取决于你最终的分析人群定义。
举个例子:如果你想分析的是“有完整人口学信息和血压测量结果的成年人”,那就从DEMO_J里筛出RIDAGEYR >= 20的样本,再和BPX_J做内连接。这样出来的行数只会少不会多,且每一行都对应一个真实完成过两类检查的受访者。
merged = pd.merge( demo, bpx, on='SEQN', how='inner', validate='one_to_one' )这里有个很实际的坑:DEMO在每个周期内是每人一行,主键唯一,但有些检查文件不一定。比如某些饮食回忆数据在个别周期里做过两次访问,SEQN会出现两次;还有一些重复测量文件本来就会产生多行。所以合并前一定要先跑一下df['SEQN'].duplicated().any(),否则合并后的行数会莫名其妙膨胀。
3.2 跨周期合并时,先统一列名再纵向拼接
如果你想做多年份的汇总分析,比如把2015-2018这两个周期拼在一起,处理方式完全不一样。这个时候不再是横向merge,而是“先清洗每个周期,再纵向concat”。
纵向拼接前有两件必做的事:第一,把各周期里同一个变量的列名统一,最好去掉后缀;第二,生成一个cycle列,记录每行来自哪个周期。否则拼完以后你根本不知道某个样本是哪年的。
我一般会先对每个周期的文件做一次标准化的rename,再在做完缺失值处理后concat到一起。批量处理时可以直接用glob把多个周期的XPT文件读进来循环,但注意每次循环都要先把cycle列填好。
4. 缺失值清洗:从识别特殊编码到处理低于检出限
4.1 把777、888这类特殊值映射成NaN
清洗缺失值的第一步,就是把Codebook里的特殊数值标记统一转换成np.nan。这个动作不能只做一次,而是要把整个文件里的所有变量都过一遍。我习惯的做法是先读取Codebook里列出的特殊编码,整理成变量到取值列表的映射字典,然后用replace统一替换。
import numpy as np special_missing_map = { 'BPXSY1': [777, 888], 'BPXDI1': [777, 888], # 其他变量按实际Codebook补充 } for col, vals in special_missing_map.items(): if col in merged.columns: merged[col] = merged[col].replace(vals, np.nan)需要注意一点:并不是所有三位数都是缺失值。有些变量里888可能就是真实合理值,全部靠数据字典确认。万一某个变量在Codebook里没写特殊缺失说明,那就别动它。
4.2 区分“真缺失”和“结构性缺失”
这是NHANES清洗里最容易影响分析质量的一步。所谓结构性缺失,就是这个人本来就不属于该变量的目标人群,比如男性问卷里的怀孕相关变量、20岁以下受访者的成人饮食问题。这类缺失代表的是“不适用”,而不是“没测到”。
处理结构性缺失时,把整个变量置成NaN是可以的,但你在做人群筛选时最好直接把人限定在目标子样本里。比如分析血压时,先筛掉没有血压读数的人,再分析,这样剩下的缺失通常就是个别遗漏,比例很低。
我每次清洗完都会跑一个缺失率报告,看看每个核心变量的缺失比例是否合理。如果某个变量缺失率超过50%,那大概率是结构性缺失,要回头确认合并方式对不对。
4.3 实验室数据里的“低于检出限”是另一个世界
如果你是做环境暴露、营养生化这类分析,NHANES实验室文件里还有一类特别容易踩坑的标记——低于检出限(LOD)。很多实验室变量并不会直接给你一个浓度值,而是给一个类似0.001这种等于或接近检测下限的值,同时数据字典里会单独列出这个变量的检出限。
很多人看到这些值以为是小到可以忽略的正常值,直接拿去做统计分析。这个做法在医学论文审稿人眼里是有问题的。正确的常见做法是按分析目的专门处理,比如用LOD除以根号2作为替代值,或者用专门处理删失数据的模型。这个已经不是pandas能搞定的范畴了,但清洗阶段至少要把这些“低值”识别出来,建议单独加一个标记列,不要混在日常缺失里面处理。
5. 权重与抽样设计变量,清洗阶段就要留好
5.1 WTMEC2YR、SDMVPSU、SDMVSTRA分别管什么
NHANES是复杂抽样设计,不是简单随机抽样,忽略权重和抽样设计直接跑统计,结果很容易偏。好消息是,你清洗阶段不需要真的去计算什么,只需要确保三个关键列没有被误删:
WTMEC2YR:两年的检查样本权重,一般做人口学和体检数据合并分析时用这个权重。SDMVPSU:抽样主要抽样单元,方差估计要用到。SDMVSTRA:分层变量,同样是方差估计必备。
这三个变量都在DEMO_*.XPT文件里,做merge以后它们会自动带进来,但如果你做纵向concat,千万别顺手当成“冗余列”删掉。后面用survey包或者weights参数建模的时候,全指望它们。
5.2 跨周期合并后的权重调整
如果你把多个两年周期拼在一起当一个大样本,那么原来每个样本都带一个“两年权重”WTMEC2YR,直接作为最终权重使用是不合适的。因为合并四个周期后,样本量大约是单个周期的四倍,而每个周期内部的权重却还是按两年设计来计算的。
常见的做法是,把合并后的权重除以合并的周期数。比如合并两个周期,权重因子是1/2;合并四个周期,权重因子是1/4。具体在模型里要不要再乘以某种缩放常数,看你用的统计分析软件的习惯。总之,跨周期合并数据绝不能只拼行,权重调整必须同步做,否则你得到的置信区间和人口代表性都有问题。
关于这一点,NHANES官方分析指南里有更严谨的论述,我建议用到多周期合并时直接去查对应版本的方法学文档,比自己琢磨靠谱得多。
6. 可直接复用的清洗流水线代码
6.1 构建一个2017-2018血压分析数据集的完整代码
这一节我直接把上面所有步骤串成一套可运行的流水线。目标是从DEMO_J.XPT和BPX_J.XPT两个文件出发,构建一个“20岁及以上成年人、有完整年龄和血压测量”的分析数据集。
import pandas as pd import numpy as np # 1. 读取文件 demo = pd.read_sas('DEMO_J.XPT', format='xport') bpx = pd.read_sas('BPX_J.XPT', format='xport') # 2. 保留核心变量,减少内存占用 demo_cols = ['SEQN', 'RIAGENDR', 'RIDAGEYR', 'RIDRETH1', 'DMDEDUC2', 'WTMEC2YR', 'SDMVPSU', 'SDMVSTRA'] bpx_cols = ['SEQN', 'BPXSY1', 'BPXDI1', 'BPXPLS'] demo = demo[demo_cols].copy() bpx = bpx[bpx_cols].copy() # 3. 缺失值特殊编码替换(按实际Codebook补充) special_missing = { 'BPXSY1': [777, 888], 'BPXDI1': [777, 888], 'BPXPLS': [777, 888], } for col, vals in special_missing.items(): if col in bpx.columns: bpx[col] = bpx[col].replace(vals, np.nan) # 4. 按分析人群筛选:20岁及以上成年人 demo = demo[demo['RIDAGEYR'] >= 20].copy() # 5. 横向合并人口学与血压检查数据 merged = pd.merge(demo, bpx, on='SEQN', how='inner', validate='one_to_one') # 6. 缺失率检查 missing_report = merged.isna().mean().round(4) print(missing_report[missing_report > 0]) # 7. 保存清洗后数据 merged.to_csv('nhanes_2017_2018_bp_clean.csv', index=False)这段代码跑完,你会得到一个主键唯一、包含人口学变量、血压变量和抽样设计变量的干净数据框。如果missing_report里出现某个核心变量缺失比例异常高,先回查第2步的字典映射和筛选逻辑。
6.2 清洗完之后的四项自检
我不止一次见过清洗流程很完整但结果不能用的情况,所以最后沉淀一个自检清单,每次跑完数据都过一遍:
- 行数是否合理:成年人样本数量级应该在几千行,如果只剩几百行,多半是merge时把样本误伤删掉了。
- SEQN是否唯一:
merged['SEQN'].duplicated().any()必须为False,否则纵向拼接或者重复测量文件混入会有问题。 - 核心变量缺失率是否可解释:缺失率突然升高时,去Codebook里找这个变量的适用人群条件。
- 权重列是否完整:
WTMEC2YR出现NaN或负数时,说明该样本可能来自非检查子样本,需要回看合并方式。
这套自检流程虽然简单,但能拦下绝大多数低级错误。
我自己现在处理NHANES数据时,已经把“清洗脚本+数据字典版本+缺失率报告”作为一个整体固化了,每次分析完都会同步留档。毕竟NHANES这种大型公共数据库,最怕的不是代码写不出来,而是清洗出来的数据和原始文档对不上。把这份工作做到规范,后面统计分析阶段会省掉大量反复核对的时间。
本文还有配套的精品资源,点击获取