ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

NHANES数据清洗实战:从XPT到可分析宽表的完整指南

NHANES数据清洗实战:从XPT到可分析宽表的完整指南 简介这份资源是面向公共卫生、流行病学与数据分析学习者的NHANES数据清洗实战代码包针对美国国家健康与营养调查数据库体量大、变量杂、缺失多等痛点提供从原始数据到可分析数据集的完整处理思路。压缩包共19个文件约149.99MB以csv数据文件为主辅以R脚本、Shell脚本、HTML说明页、PNG图表与Markdown文档覆盖数据生成、合并、清洗、插补与协变量筛选等环节。已有450人学习下载。读者可参考其中的R代码借助tidyverse与haven完成XPT格式数据的读取与合并利用mice包对缺失值进行多重插补并依据示例完成协变量筛选与列名优化最终得到结构清晰、可直接用于统计建模的干净数据集适合需要快速上手NHANES清洗流程的研究者与数据分析初学者。1. NHANES 数据清洗为什么你拿到的原始表一导入就翻车NHANES 数据清洗这件事真正卡住人的从来不是模型而是把官方 XPT 文件读进来之后那一堆莫名其妙的缺失值、特殊编码和权重列。国家健康与营养调查NHANES每两年一轮公开的 SAS XPT 文件里缺失不是 NaN而是.、空字符串、甚至7777、9999这类占位码血压、体测、问卷分散在不同文件靠 SEQN 才能拼起来。很多人第一次用 pandas 读进来describe()一看全是 object直接懵。这篇笔记面向已经拿到 NHANES 原始文件、准备做队列分析或建模的从业者把清洗链路拆成可复现的步骤读文件、认编码、拼表、处理权重、导出干净宽表。代码全部基于 pandas能直接抄。目标只有一个——让你手里的 NHANES 从“能读”变成“能用”。2. 从 XPT 到 DataFrame读文件与识别特殊编码2.1 为什么 pandas 读 XPT 会给你一堆 objectNHANES 官方提供的是 SAS transport 格式.XPT不是 CSV。pandas 从 0.21 起内置read_sas能直接读 XPT但读出来的列类型经常是 object原因是 XPT 里数值列一旦混入缺失标记整列就被当成字符。更麻烦的是NHANES 的缺失码不统一连续变量常用.表示缺失分类变量用7/9/77/99/777/999表示“拒答”“不知道”“未检测”。如果你不先处理这些码直接mean()7777 会把均值拉到天上。常见做法是读进来先不转类型保留原始值用一份“缺失码字典”逐列替换再统一转 float。下面是我一般会用的最小读取脚本。import pandas as pd import numpy as np # 读单个 XPTformatxport 是 pandas 对 XPT 的识别方式 df pd.read_sas(DEMO_J.XPT, formatxport) # 先看列类型和缺失情况别急着转 print(df.dtypes) print(df.isna().sum()) # NHANES 常见缺失码7/9/77/99/777/999/7777/9999 以及 . missing_codes [7, 9, 77, 99, 777, 999, 7777, 9999] # 只对数值列做替换避免把分类列的真实编码误伤 for col in df.select_dtypes(include[np.number]).columns: df[col] df[col].replace(missing_codes, np.nan) # 统一转 float方便后续计算 df df.astype({c: float64 for c in df.select_dtypes(include[np.number]).columns})逻辑说明read_sas的formatxport是关键不写会报错。替换缺失码时只对数值列操作因为分类列里 7 和 9 可能是真实类别比如教育程度不能一刀切。参数上missing_codes列表按 NHANES 文档常见值整理但每轮调查的码可能不同读之前最好翻一下对应文件的 codebook。提示不要用df.replace(missing_codes, np.nan)全局替换分类列会被误伤。先select_dtypes再替换是血泪经验。2.2 用 codebook 核对每一列的合法取值范围光靠通用缺失码不够。NHANES 每个文件都有一份 codebook里面写清楚每列的取值比如RIAGENDR只有 1男和 2女RIDAGEYR是 0-80 的连续年龄80 以上统一记 80。如果你发现某列出现了 codebook 里没有的值要么是缺失码没清干净要么是读文件时列错位。我一般会写一个校验函数把 codebook 里的合法范围写成字典跑一遍全列检查。# 以 DEMO_J 为例定义关键列的合法范围 valid_ranges { RIAGENDR: {1, 2}, RIDAGEYR: set(range(0, 81)), RIDRETH1: {1, 2, 3, 4, 5}, DMDEDUC2: {1, 2, 3, 4, 5, 7, 9}, # 7/9 是拒答/不知道保留 } def check_valid(df, ranges): for col, allowed in ranges.items(): if col not in df.columns: continue bad df[~df[col].isin(allowed) df[col].notna()] if len(bad) 0: print(f{col} 有 {len(bad)} 行超出合法范围示例{bad[col].unique()[:5]}) check_valid(df, valid_ranges)逻辑说明isin判断是否在合法集合内notna()排除已处理的缺失。参数valid_ranges需要你根据实际使用的文件手动整理不同周期如 2017-2018 和 2019-2020的列名和取值可能微调。这一步能提前发现列错位或编码误读比后面建模时才发现问题省事得多。2.3 把 SEQN 当成主键读文件时就要检查唯一性NHANES 所有文件都靠 SEQN受访者序号关联。读进来第一件事是确认 SEQN 唯一且没有重复。如果重复要么是文件读串了要么是某些文件本身有多行比如实验室重复检测。我见过有人直接merge结果行数翻倍查了半天才发现是某个文件里同一 SEQN 出现两次。# 检查 SEQN 唯一性 assert df[SEQN].is_unique, SEQN 不唯一检查文件是否读错 # 如果确实有多行如重复检测先聚合再合并 # 例如取均值df df.groupby(SEQN, as_indexFalse).mean()逻辑说明is_unique是快速断言失败就直接停别往下走。如果业务上确实需要保留多行合并前先想清楚聚合规则否则后续权重计算会出错。参数上groupby(SEQN).mean()只适合数值列分类列要用众数或保留首行。3. 拼表与权重把分散文件合成一张分析宽表3.1 按 SEQN 横向合并顺序和列名冲突怎么处理NHANES 一轮调查有几十个文件人口学DEMO、体测BMX、血压BPX、问卷Q、实验室LAB。做分析通常只需要其中几个。合并时用pd.mergeonSEQNhowinner还是outer取决于你的研究问题。如果只关心有完整体测和血压的人用 inner如果想保留所有受访者用 outer 再处理缺失。列名冲突是常见坑不同文件可能有同名列比如RIDAGEYR只在 DEMO 里但LBXTC和LBDTCSI可能同时出现在不同 lab 文件。合并前先看列名冲突的加后缀。demo pd.read_sas(DEMO_J.XPT, formatxport) bmx pd.read_sas(BMX_J.XPT, formatxport) bpx pd.read_sas(BPX_J.XPT, formatxport) # 只保留需要的列减少冲突 demo demo[[SEQN, RIAGENDR, RIDAGEYR, RIDRETH1, WTMEC2YR]] bmx bmx[[SEQN, BMXBMI, BMXWT, BMXHT]] bpx bpx[[SEQN, BPXSY1, BPXDI1]] # 横向合并inner 保留三个文件都有的受访者 df demo.merge(bmx, onSEQN, howinner).merge(bpx, onSEQN, howinner) print(df.shape) print(df.head())逻辑说明先选列再合并能避免大量无关列和重名。howinner会丢掉任一文件缺失的受访者如果你的研究需要最大样本改成outer并在后续用dropna控制。参数上WTMEC2YR是 MEC 检查权重后面算加权时要用。3.2 权重列不是装饰品WTMEC2YR 和 WTINT2YR 怎么选NHANES 是复杂抽样设计官方明确要求分析时使用权重否则结果有偏。常见权重有两类WTINT2YR访谈权重和WTMEC2YRMEC 检查权重。如果你用的变量来自问卷用访谈权重如果来自体测或实验室用 MEC 权重。合并多个周期时权重要除以周期数比如 4 年权重 2 年权重 / 2。# 假设只用 2017-2018 一轮直接用 WTMEC2YR df[weight] df[WTMEC2YR] # 如果是 2017-2020 两轮合并权重除以 2 # df[weight] df[WTMEC2YR] / 2 # 加权均值示例血压收缩压 weighted_mean np.average(df[BPXSY1].dropna(), weightsdf.loc[df[BPXSY1].notna(), weight]) print(f加权平均收缩压{weighted_mean:.2f})逻辑说明np.average的weights参数要求与数据等长所以先用notna()对齐。参数上权重列本身不能有缺失如果有要么剔除要么用多重插补。常见误用是直接df[BPXSY1].mean()在 NHANES 里这个值几乎不可信。注意权重列在合并后可能因为 inner join 丢失部分行算加权前先确认权重列没有 NaN。3.3 分类变量重编码把 1/2 变成可读标签NHANES 的分类变量都是数字编码比如性别 1/2种族 1-5。建模时可以用数字但做描述统计或画图时可读标签更直观。重编码时保留原始列新建一列避免覆盖后无法回溯。# 性别重编码 df[gender] df[RIAGENDR].map({1: Male, 2: Female}) # 种族重编码 race_map {1: Mexican American, 2: Other Hispanic, 3: Non-Hispanic White, 4: Non-Hispanic Black, 5: Other Race} df[race] df[RIDRETH1].map(race_map) # 检查是否有未映射的值 print(df[gender].isna().sum(), df[race].isna().sum())逻辑说明map遇到未定义值会返回 NaN正好用来检查是否有遗漏编码。参数上race_map按 NHANES 官方文档整理不同周期可能微调。重编码后建议value_counts()看一眼分布确认没有意外缺失。4. 缺失值处理与异常值排查别让 7777 混进均值4.1 缺失机制判断MCAR、MAR 还是 MNARNHANES 的缺失不是随机的。体测数据缺失往往和年龄、健康状况相关MAR实验室数据可能因为样本量不足而缺失MNAR。直接dropna()会引入选择偏倚。常见做法是先统计每列缺失比例低于 5% 的可以考虑剔除高于 5% 的用多重插补或加权调整。# 缺失比例统计 missing_ratio df.isna().mean().sort_values(ascendingFalse) print(missing_ratio[missing_ratio 0]) # 对缺失低于 5% 的列直接删行 cols_low_missing missing_ratio[missing_ratio 0.05].index df_clean df.dropna(subsetcols_low_missing) # 对缺失较高的列保留并用标记列记录 df_clean[BPXSY1_missing] df_clean[BPXSY1].isna().astype(int)逻辑说明missing_ratio帮你快速定位问题列。dropna(subset...)只删指定列缺失的行不影响其他列。参数上5% 是经验阈值实际按研究领域调整。加缺失标记列是为了后续建模时把缺失本身当成特征。4.2 异常值生理范围之外的数先别急着删血压、BMI、年龄都有生理合理范围。比如收缩压低于 60 或高于 250BMI 低于 10 或高于 80通常是录入错误或设备问题。但别直接删先标记看比例再决定。# 定义生理合理范围 df_clean[BPXSY1_outlier] ((df_clean[BPXSY1] 60) | (df_clean[BPXSY1] 250)).astype(int) df_clean[BMXBMI_outlier] ((df_clean[BMXBMI] 10) | (df_clean[BMXBMI] 80)).astype(int) # 统计异常比例 print(df_clean[[BPXSY1_outlier, BMXBMI_outlier]].mean()) # 如果比例很低1%可以剔除否则保留并标记 df_clean df_clean[df_clean[BPXSY1_outlier] 0]逻辑说明先标记再决定避免误删真实极端值。参数上范围参考临床指南不同研究可调整。剔除后记得重新检查权重分布防止样本偏倚。4.3 用 describe 和箱线图快速定位翻车列清洗完跑一遍describe()看 min/max 是否合理。如果某列 max 是 9999说明缺失码没清干净。箱线图能直观看到离群点但 NHANES 数据量大画图前先采样。# 描述统计 print(df_clean.describe().T[[min, max, mean, std]]) # 采样后画箱线图需要 matplotlib import matplotlib.pyplot as plt sample df_clean.sample(1000, random_state42) sample[[BPXSY1, BMXBMI]].plot(kindbox, subplotsTrue, layout(1, 2)) plt.show()逻辑说明describe().T转置后更易读。采样是为了避免画图卡顿。参数上random_state固定保证可复现。如果 min/max 明显异常回到第 2 章检查缺失码替换逻辑。5. 避坑与排查NHANES 清洗里最容易翻车的 5 个点5.1 现象合并后行数暴涨 → 原因SEQN 重复或笛卡尔积 → 解决合并前断言唯一性合并前对每个文件跑assert df[SEQN].is_unique。如果某个文件确实有多行如重复测量先聚合到 SEQN 级别再合并。别用merge后drop_duplicates那会丢数据。5.2 现象加权均值和不加权差很多 → 原因权重列选错或没对齐 → 解决确认权重类型并检查缺失问卷变量用WTINT2YR体测/实验室用WTMEC2YR。算加权前用df[weight].isna().sum()检查有缺失就剔除对应行。多周期合并时权重记得除以周期数。5.3 现象分类变量出现 7/9 → 原因缺失码误当真实类别 → 解决按 codebook 区分不是所有 7/9 都是缺失。比如DMDEDUC2里 7 是“拒答”9 是“不知道”但RIAGENDR里没有 7/9。处理前翻 codebook别全局替换。5.4 现象年龄 80 以上全是 80 → 原因NHANES 对 80 统一截断 → 解决建模时考虑截断影响RIDAGEYR在 80 以上统一记 80这是隐私保护设计。做年龄连续分析时80 岁以上的信息丢失建议分组或加标记。别把它当真实年龄算均值。5.5 现象XPT 读进来列名带下划线或大小写不一致 → 原因不同周期文件命名差异 → 解决统一列名后再合并NHANES 不同周期的列名可能微调如LBXTCvsLBXTC。合并前先df.columns df.columns.str.upper()再核对 codebook。列名不一致会导致 merge 后出现_x/_y后缀容易搞混。6. 进阶把清洗流程封装成可复用的管道清洗代码写一次容易复用难。我一般会把整个流程拆成三个函数read_nhanes(file)负责读文件和替换缺失码merge_nhanes(files, how)负责按 SEQN 合并clean_nhanes(df)负责缺失标记和异常值。这样换一轮数据只需改文件名。def read_nhanes(path, missing_codesNone): df pd.read_sas(path, formatxport) if missing_codes is None: missing_codes [7, 9, 77, 99, 777, 999, 7777, 9999] for col in df.select_dtypes(include[np.number]).columns: df[col] df[col].replace(missing_codes, np.nan) return df def merge_nhanes(dfs, howinner): from functools import reduce return reduce(lambda l, r: pd.merge(l, r, onSEQN, howhow), dfs) def clean_nhanes(df): df df.copy() df[BPXSY1_outlier] ((df[BPXSY1] 60) | (df[BPXSY1] 250)).astype(int) df[BMXBMI_outlier] ((df[BMXBMI] 10) | (df[BMXBMI] 80)).astype(int) return df逻辑说明reduce把多个 DataFrame 依次合并避免嵌套 merge 写成一坨。参数上how默认 inner按需改 outer。clean_nhanes里用copy()防止修改原数据。验证清洗结果是否可靠我习惯做两件事一是用官方发布的加权均值对照比如 CDC 公布的某年平均收缩压如果你的加权结果差超过 2 mmHg大概率权重或缺失处理有问题二是随机抽 10 个 SEQN去 NHANES 官网查原始值核对清洗后是否一致。这个习惯帮我抓过好几次列错位的 bug。最后说个我自己的教训早期做 NHANES 时嫌麻烦没加权重跑出来的 BMI 和血压关系被审稿人直接质疑返工重跑花了两周。从那以后权重列和 SEQN 唯一性检查成了我读文件后的固定动作。希望帮到你。本文还有配套的精品资源点击获取
返回列表