NHANES数据清洗代码包:从XPT到宽表的完整流程与避坑指南

📅 发布时间:2026/10/8 20:25:54
NHANES数据清洗代码包:从XPT到宽表的完整流程与避坑指南
简介这份资源面向公共卫生、流行病学与数据分析方向的研究者及学生提供NHANES数据库数据清洗的完整代码实现帮助解决从原始XPT数据到可分析数据集的流程问题。压缩包共19个文件约149.99MB以csv数据文件、R脚本、sh运行脚本为主另含html说明页、png分布图与md文档覆盖原始数据、合并、清洗到输出的各阶段产物。已有450人学习下载。内容围绕数据选择、合并、清洗、插补与协变量筛选展开借助tidyverse、haven与mice等R包完成多周期数据整合、缺失值多重插补及无效协变量剔除并给出列名优化建议。目录按00_rawdata、01_merge、03_output等模块组织便于对照复现适合需要快速搭建NHANES清洗流程、减少重复调试的读者参考使用。1. NHANES 数据清洗代码包从原始 XPT 到可分析宽表的完整路径如果你从 NHANES 官网拖过几个周期的数据大概经历过这种崩溃下载下来是.XPT格式用 pandas 读进去列名全是英文缩写缺失值编码五花八门不同周期的变量名还对不上合并完发现样本量对不上号。这套 NHANES 数据清洗代码包就是冲着这些事来的——它把「读原始文件 → 变量重编码 → 缺失值处理 → 多周期合并 → 导出分析宽表」整条链路封装成了可复用的脚本。适合做流行病学、营养学、环境暴露方向的分析人员也适合刚接触 NHANES 公共数据库、想跳过格式折腾直接进入建模阶段的人。代码基于 pandas 和 numpy不依赖冷门库拿到就能跑。2. 拆开代码包模块划分与核心清洗逻辑2.1 目录结构与各文件职责拿到代码包先别急着跑花两分钟看清楚每个文件干什么后面改参数才不会迷路。典型的结构是这样nhanes_clean/ ├── config.py # 周期、变量映射、文件路径配置 ├── loader.py # XPT/SAS 文件读取与列名标准化 ├── recode.py # 缺失值编码、分类变量重编码 ├── merge.py # 多周期纵向合并与键对齐 ├── export.py # 导出 CSV/Parquet/Stata 格式 └── run_pipeline.py # 串联全流程的入口脚本config.py是整个流程的「控制面板」周期列表、变量名映射表、缺失值编码规则都集中在这里。loader.py负责把.XPT读成 DataFrame 并统一列名大小写。recode.py处理 NHANES 特有的缺失值编码——这是最容易翻车的地方后面单独讲。merge.py做跨周期合并export.py负责输出。入口脚本run_pipeline.py按顺序调用你只需要改config.py就能适配自己的研究变量。2.2 读取 XPT 文件与列名标准化NHANES 的原始数据以 SAS Transport 格式.XPT分发pandas 可以直接读但有几个细节要注意。下面是我常用的读取封装import pandas as pd import os def load_xpt(data_dir, cycle, table_name): 读取指定周期的 XPT 文件并标准化列名。 data_dir: 数据根目录 cycle: 周期标识如 2017-2018 table_name: 表名如 DEMO_J fname f{table_name}.XPT fpath os.path.join(data_dir, cycle, fname) if not os.path.exists(fpath): raise FileNotFoundError(f找不到文件: {fpath}) df pd.read_sas(fpath, formatxport, encodingutf-8) # 列名统一转小写去掉前后空格 df.columns [c.strip().lower() for c in df.columns] # SEQN 是受访者唯一标识必须保留为整数 df[seqn] df[seqn].astype(int) return df逻辑说明pd.read_sas的formatxport参数专门用于.XPT文件不要用read_csv硬读。encodingutf-8在大多数情况下没问题但如果遇到中文路径或特殊字符报错可以换成latin1试试。列名统一小写是为了后续合并时避免SEQN和seqn对不上的问题。SEQN强制转 int 是因为它在不同文件里可能被读成 float合并时会因为类型不一致产生笛卡尔积——这个坑我踩过样本量突然翻倍排查了半天。2.3 缺失值编码识别与重编码NHANES 的缺失值不是NaN而是用特定数字编码的。常见的有7和9表示「拒绝回答」和「不知道」777和999表示「不知道」和「拒绝回答」的扩展编码7777和9999同理。如果不处理这些值会被当成真实数值参与计算均值直接偏到姥姥家。import numpy as np # 不同变量的缺失值编码可能不同按变量配置 MISSING_CODES { ridageyr: [7, 9, 77, 99, 777, 999], # 年龄 riagendr: [7, 9], # 性别 bmxbmi: [7, 9, 77, 99], # BMI lbxglu: [777, 999], # 血糖 } def recode_missing(df, code_map): 将指定变量的缺失值编码替换为 np.nan。 code_map: {变量名: [缺失值编码列表]} df df.copy() for var, codes in code_map.items(): if var in df.columns: df[var] df[var].replace(codes, np.nan) return df参数说明MISSING_CODES字典需要根据你实际使用的变量来配置。NHANES 官方文档里每个变量的缺失值编码都写在 codebook 里不要凭感觉猜。比如年龄变量RIDAGEYR的缺失编码是 7 和 9但RIDAGEYR本身是连续变量7 和 9 也可能是真实年龄——所以 NHANES 对连续变量通常用 777/999 这种不可能出现的值来标记缺失。替换时用replace而不是fillna因为fillna只能处理NaN对数字编码无效。2.4 多周期纵向合并的键对齐NHANES 每两年一个周期变量名会变。比如 2017-2018 周期的 BMI 叫BMXBMI2015-2016 也叫BMXBMI但有些变量在不同周期里名字不同。合并时以SEQN为键但要注意不同周期的SEQN不会重复直接concat就行不需要merge。def merge_cycles(df_list, keep_vars): 纵向合并多个周期的数据只保留指定变量。 df_list: 各周期 DataFrame 列表 keep_vars: 需要保留的变量名列表统一后的名字 merged [] for df in df_list: # 只保留 SEQN 和需要的变量 cols [seqn] [v for v in keep_vars if v in df.columns] merged.append(df[cols]) result pd.concat(merged, ignore_indexTrue) # 去重同一个 SEQN 只保留一条 result result.drop_duplicates(subsetseqn, keepfirst) return result逻辑说明pd.concat默认按列名对齐如果某个周期缺少某个变量该列会自动填NaN。drop_duplicates是为了防止同一个受访者被重复纳入——虽然理论上SEQN跨周期不重复但如果你不小心把同一个周期的数据读了两遍这一步能兜底。keep_vars列表建议在config.py里维护不要硬编码在函数里。3. 跑通全流程从配置到导出可分析宽表3.1 配置文件怎么写config.py是整个流程的入口改这里就能适配不同的研究需求。一个典型的配置长这样# config.py # 数据根目录 DATA_DIR ./nhanes_data # 需要处理的周期 CYCLES { 2015-2016: {demo: DEMO_I, bmi: BMX_I, lab: GLU_I}, 2017-2018: {demo: DEMO_J, bmi: BMX_J, lab: GLU_J}, } # 需要保留的变量统一后的名字 KEEP_VARS [seqn, ridageyr, riagendr, bmxbmi, lbxglu] # 缺失值编码映射 MISSING_CODES { ridageyr: [777, 999], riagendr: [7, 9], bmxbmi: [777, 999], lbxglu: [777, 999], } # 导出格式csv / parquet / stata EXPORT_FORMAT csv EXPORT_PATH ./output/nhanes_clean.csv参数说明CYCLES字典的键是周期标识值是各模块对应的表名。NHANES 的表名有规律DEMO是人口学BMX是体格测量GLU是血糖。后缀字母I对应 2015-2016J对应 2017-2018以此类推。KEEP_VARS里seqn必须保留其他变量按研究需求增减。EXPORT_FORMAT选parquet读写更快但如果你后续要用 Stata 或 SPSS选csv或stata更省事。3.2 入口脚本串联与运行run_pipeline.py把各个模块串起来跑一条命令就能出结果# run_pipeline.py from config import DATA_DIR, CYCLES, KEEP_VARS, MISSING_CODES, EXPORT_FORMAT, EXPORT_PATH from loader import load_xpt from recode import recode_missing from merge import merge_cycles from export import export_data def main(): all_cycles [] for cycle, tables in CYCLES.items(): # 读取每个周期的多个表 dfs {} for key, table_name in tables.items(): dfs[key] load_xpt(DATA_DIR, cycle, table_name) # 以 seqn 为键横向合并同一周期的多个表 df_cycle dfs[demo] for key in [bmi, lab]: if key in dfs: df_cycle df_cycle.merge(dfs[key], onseqn, howleft) # 缺失值重编码 df_cycle recode_missing(df_cycle, MISSING_CODES) all_cycles.append(df_cycle) # 纵向合并所有周期 final_df merge_cycles(all_cycles, KEEP_VARS) # 导出 export_data(final_df, EXPORT_PATH, fmtEXPORT_FORMAT) print(f清洗完成样本量: {len(final_df)}变量数: {len(final_df.columns)}) if __name__ __main__: main()逻辑说明同一周期内多个表用merge横向拼接howleft保证以人口学表为基准其他表匹配不上的填NaN。跨周期用concat纵向堆叠。export_data函数根据fmt参数选择导出格式内部用to_csv、to_parquet或to_stata实现。运行命令就是python run_pipeline.py前提是你已经按config.py里的路径放好了数据文件。3.3 导出格式选择与验证导出后别急着关终端花一分钟验证数据质量。我一般会跑这几行import pandas as pd df pd.read_csv(./output/nhanes_clean.csv) print(df.shape) # 样本量和变量数 print(df.isnull().sum()) # 各变量缺失情况 print(df.describe()) # 连续变量分布 print(df[riagendr].value_counts()) # 分类变量频数参数说明df.shape确认样本量是否符合预期——NHANES 每个周期约 9000-10000 人两个周期合并后应该在 18000-20000 左右。isnull().sum()看缺失比例如果某个变量缺失超过 50%要考虑是否纳入分析。describe()检查连续变量的均值和范围如果年龄出现 999 或 BMI 出现 777说明缺失值没替换干净。value_counts()检查分类变量性别应该只有 1 和 2 两个值。4. 避坑指南NHANES 清洗中五个血泪教训4.1 缺失值编码没替换干净现象跑完清洗流程describe()显示年龄均值 45但最大值 999BMI 最大值 777。原因MISSING_CODES配置不完整或者某些变量的缺失编码和预期不一致。NHANES 不同周期的同一变量缺失编码可能不同。解决对照官方 codebook 逐个变量核对缺失编码不要凭经验猜。可以在recode.py里加一行日志打印每个变量替换前后的唯一值数量替换后唯一值数量应该减少。4.2 合并后样本量翻倍现象两个周期各 9000 人合并后变成 36000 人。原因SEQN类型不一致一个周期是 int另一个是 floatpandas 认为是不同的键产生了笛卡尔积。或者同一周期的数据被重复读取。解决在load_xpt里强制df[seqn] df[seqn].astype(int)。合并前用df[seqn].duplicated().sum()检查是否有重复键。4.3 变量名跨周期不一致现象合并后某个变量全是NaN但明明每个周期都有这个变量。原因不同周期的变量名大小写或拼写不同比如BMXBMI和BMXBMI看起来一样但实际一个是BMXBMI另一个是BMXBMI末尾有空格。解决在load_xpt里统一strip().lower()。如果变量名确实不同在config.py里加一个映射表读取后重命名。4.4 分类变量编码含义搞反现象性别变量1和2你以为1是男结果是女。原因NHANES 的编码含义写在 codebook 里不同变量编码规则不同。性别1Male, 2Female但有些变量1Yes, 2No还有些1No, 2Yes。解决每个分类变量在使用前查 codebook在config.py里加一个LABEL_MAP字典清洗时直接映射成可读标签。4.5 权重变量没纳入现象清洗完的数据直接跑回归结果和文献对不上。原因NHANES 是复杂抽样设计分析时必须使用权重变量如WTMEC2YR、SDMVPSU、SDMVSTRA否则结果有偏。解决在KEEP_VARS里加入权重变量分析时用survey包或statsmodels的加权功能。权重变量在不同周期的名字可能不同需要单独处理。5. 进阶技巧用 Parquet 加速与自动化验证清洗流程跑通之后下一步是让它更快、更稳。我一般会把中间结果存成 Parquet 格式读取速度比 CSV 快 5-10 倍而且自带类型信息不会出现SEQN被读成 float 的问题。在export.py里加一个分支def export_data(df, path, fmtcsv): if fmt parquet: df.to_parquet(path, indexFalse) elif fmt stata: df.to_stata(path, write_indexFalse) else: df.to_csv(path, indexFalse)参数说明to_parquet需要pyarrow或fastparquet库pip install pyarrow即可。indexFalse避免把 pandas 的索引列写进去否则下次读取会多一列Unnamed: 0。to_stata对变量名有长度限制最多 32 个字符如果变量名太长会报错需要提前重命名。自动化验证我习惯用pytest写几个断言每次跑完流程自动检查# test_pipeline.py import pandas as pd def test_no_missing_codes(): df pd.read_parquet(./output/nhanes_clean.parquet) assert df[ridageyr].max() 100, 年龄出现异常值 assert df[bmxbmi].max() 100, BMI 出现异常值 assert set(df[riagendr].dropna().unique()) {1, 2}, 性别编码异常 def test_sample_size(): df pd.read_parquet(./output/nhanes_clean.parquet) assert len(df) 10000, 样本量过少可能合并出错逻辑说明test_no_missing_codes检查连续变量是否还有残留的缺失编码test_sample_size检查合并后样本量是否合理。跑pytest test_pipeline.py就能自动验证。这套组合拳打下来每次更新数据或调整变量跑一遍测试就知道有没有翻车。从那以后我每次处理 NHANES 数据都强制走一遍「读原始文件 → 检查缺失编码 → 合并 → 验证样本量 → 导出」的完整流程再也不拍脑袋直接read_csv硬上了。希望帮到你。本文还有配套的精品资源点击获取