據(jù)自動化下載與整理:Python腳本從手動一周到十幾分鐘)
簡介NHANES數(shù)據(jù)庫下載整理源碼面向醫(yī)學研究者、公共衛(wèi)生分析師及R語言入門者解決全國健康和營養(yǎng)調(diào)查數(shù)據(jù)的下載、清洗與整合難題。包內(nèi)共5個文件含R分析腳本、HTML預覽頁、Markdown說明文檔以及gitignore、inscode等配套文件整體僅8KB輕量聚焦。已有232人學習下載適合需要快速掌握NHANES數(shù)據(jù)獲取流程的用戶。腳本演示了兩種下載方案一是官網(wǎng)手動下載后經(jīng)R讀取處理二是通過nhanesA包批量拉取數(shù)據(jù)集并覆蓋缺失值處理、多周期數(shù)據(jù)合并以及導出為CSV等常用步驟可幫助初學者避開網(wǎng)絡中斷、版本更新等下載陷阱直接復用代碼完成從原始數(shù)據(jù)到分析格式的轉(zhuǎn)換。 做流行病學或營養(yǎng)學課題的人幾乎繞不開 NHANES國家健康與營養(yǎng)調(diào)查這個公共數(shù)據(jù)庫。我的課題需要用到人群膳食攝入和 BMI 的數(shù)據(jù)導師丟過來一句“你去把 NHANES 里近幾個周期的 demographics 和 dietary 數(shù)據(jù)下下來整理好”我以為就是下幾個 Excel結果打開官網(wǎng)才發(fā)現(xiàn)這個數(shù)據(jù)庫的數(shù)據(jù)按兩年一個周期、幾十個組件、幾百個文件鋪開手動下載加整理花了我整整一個星期。后來我把整個過程寫成了一套自動化源碼下載加清潔壓縮到十幾分鐘。這篇博文就把這套 NHANES 下載整理工具的設計思路、關鍵源碼和踩坑記錄完整梳理一遍適合正要接觸 NHANES 的醫(yī)學研究生、生信分析人員以及所有被大型公共數(shù)據(jù)庫折磨過的人。1. 手動下載被虐過的人都會想要一個自動化工具1.1 NHANES 的數(shù)據(jù)組織方式周期-組件-文件三層結構NHANES 的數(shù)據(jù)并不是一個“大 Excel”而是按**兩年一個周期Cycle**組織起來的。1999-2000 年是第一周期之后每兩年一輪一直到最近的 2017-2020 年周期。因為疫情原因2017-2020 這個周期被拆成了“2017-March 2020 pre-pandemic”的特殊版本文件名統(tǒng)一加了P_前綴比如人口學數(shù)據(jù)叫P_DEMO.xpt這算是近年最大的一個結構變化。每個周期內(nèi)部又按**組件Component**分成五大類Demographics人口學、Dietary膳食、Examination體檢、Laboratory實驗室和 Questionnaire問卷。每個組件下再掛若干個具體數(shù)據(jù)文件一個周期加起來少說有三四十個文件。文件名大多是縮寫比如P_DR1TOT.xpt是第一天 24 小時膳食總攝入P_BMX.xpt是體檢指標P_LBX_HDL.xpt是高密度脂蛋白膽固醇。我第一次打開官網(wǎng)的數(shù)據(jù)下載頁時看到的是整頁的表格鏈接每個鏈接背后還是一個 XPT 格式的數(shù)據(jù)文件這種文件用 Excel 直接打不開得靠 SAS、R 或 Python 來處理。再加上網(wǎng)站偶爾抽風、下載一半斷掉、文件相互關聯(lián)要按編號合并手動操作一次就知道有多折磨人。1.2 痛點在哪下載不是問題整理才是很多人以為用瀏覽器把文件點下來就完事了真正的工作量在整理。XPT 文件讀進來之后變量名全是LBX、DRX、SEQN這種縮寫沒有 codebook 根本看不懂同一周期里的多個表要靠SEQN受訪者編號橫向關聯(lián)不同周期的同一個變量編碼方式還可能有變化。你還需要處理缺失值標記、抽樣權重、合并后行數(shù)爆炸等一系列問題。我當時的訴求很明確輸入一個周期和組件參數(shù)腳本自動把該周期下所有需要的數(shù)據(jù)文件下載到本地斷點續(xù)傳下載完自動讀入、按SEQN合并、導出成干凈的 CSV 或 Parquet同時保留一份變量說明。這套邏輯單獨寫成工具以后做不同課題、換不同周期只需要改一個參數(shù)就能復用。2. 源碼架構增量下載、斷點續(xù)傳與本地數(shù)據(jù)倉庫2.1 需要用到的庫與版本我用的 Python 3.9核心庫就四個requests負責網(wǎng)絡請求BeautifulSoup解析官網(wǎng)目錄頁pandas讀取和合并 XPTtqdm顯示下載進度。這些庫都是數(shù)據(jù)分析標配安裝成本低換臺機器也能快速跑起來。pip install requests beautifulsoup4 pandas tqdm為什么不用 R雖然 R 有NHANES包可以直接加載部分內(nèi)置數(shù)據(jù)但那個包的數(shù)據(jù)是預整理的覆蓋不全尤其最新周期跟不上而且不支持批量下載和斷點續(xù)傳。Python 里requestspandas的組合更像一個完整的數(shù)據(jù)工程工具鏈后續(xù)接建模、出圖表、寫服務都順。2.2 三個核心模塊的設計整個工具我拆成三層鏈接解析層、下載層、整理層。鏈接解析層的職責是打開官方的數(shù)據(jù)目錄頁把頁面里所有指向.XPT文件的鏈接抓出來同時提取文件名。這一步要放在最前面因為官網(wǎng)的頁面結構偶爾會調(diào)整解析規(guī)則寫成一個獨立函數(shù)頁面改版時只改這里就行。下載層是最需要照顧異常的地方。我做了一個帶斷點續(xù)傳的下載函數(shù)每下載一個文件前先檢查本地是否已有同名文件如果存在且大小與服務器返回的Content-Length一致就跳過不一致或下載中斷則從本地已有大小處用Range請求繼續(xù)下載。同時設置重試機制遇到網(wǎng)絡抖動自動退避重試。整理層負責把下載好的.xpt文件讀入 pandas然后按SEQN做左連接??紤]到很多課題只用到部分變量我加了一個usecols參數(shù)允許只讀入指定的列免得內(nèi)存被撐爆。最后統(tǒng)一導出為 Parquet 或 CSV并順手生成一份變量字典 CSV方便回溯。2.3 為什么選 Python 而不是 curl 腳本如果你只是想把文件批量抓下來curl加一個循環(huán)腳本也夠用。但 NHANES 整理環(huán)節(jié)需要的數(shù)據(jù)清洗邏輯缺失值處理、編碼轉(zhuǎn)換、跨周期合并用 shell 寫會極其痛苦。Python 的優(yōu)勢在于下載是requests的事解析是BeautifulSoup的事讀數(shù)據(jù)是pandas的事每層之間接口清晰后續(xù)迭代時單獨換掉某一層不影響全局。3. 核心源碼逐段拆解3.1 解析官方頁面拿到 XPT 直鏈官網(wǎng)的數(shù)據(jù)目錄頁是一個參數(shù)化的 ASPX 頁面通過Component和CycleBeginYear兩個參數(shù)指定組件與周期。以下是我解析直鏈的核心代碼import requests from bs4 import BeautifulSoup from urllib.parse import urljoin BASE https://wwwn.cdc.gov/nchs/nhanes/search/datapage.aspx def fetch_xpt_links(component: str, cycle: int): params {Component: component, CycleBeginYear: cycle} resp requests.get(BASE, paramsparams, timeout30) resp.raise_for_status() soup BeautifulSoup(resp.text, html.parser) links [] for a in soup.select(a[href$.XPT]): links.append(urljoin(BASE, a[href].strip())) return sorted(set(links))這里有幾個細節(jié)容易踩坑。第一href可能是相對路徑必須用urljoin拼接成完整地址第二頁面里同一文件可能出現(xiàn)在多個展示位置所以最后要用set()去重第三官網(wǎng)偶爾會臨時返回一個錯誤頁這時raise_for_status()能第一時間暴露問題不至于讓你誤以為解析成功。3.2 帶斷點續(xù)傳的下載器下載是我花時間最多的部分。NHANES 的 XPT 文件單個不大但整周期下全會遇到網(wǎng)絡中斷、HTTP 403 等情況。我的下載函數(shù)實現(xiàn)了三個能力增量跳過、斷點續(xù)傳、指數(shù)退避重試。import os import time import requests HEADERS {User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64)} def download_with_resume(url: str, dest: str, max_retries: int 3) - bool: # 如果本地文件完整存在直接跳過 if os.path.exists(dest): head requests.head(url, headersHEADERS, allow_redirectsTrue, timeout30) remote_size int(head.headers.get(Content-Length, 0)) if remote_size and os.path.getsize(dest) remote_size: return False # 已存在無需下載 for attempt in range(max_retries): try: resume os.path.getsize(dest) if os.path.exists(dest) else 0 headers dict(HEADERS) if resume: headers[Range] fbytes{resume}- with requests.get(url, headersheaders, streamTrue, timeout60) as r: r.raise_for_status() mode ab if resume else wb with open(dest, mode) as f: for chunk in r.iter_content(chunk_size8192): f.write(chunk) return True except Exception as e: if attempt max_retries - 1: raise RuntimeError(f{url} 下載失敗: {e}) from e time.sleep(2 ** attempt) # 1s, 2s, 4s 退避 return False這段代碼有三個值得說的細節(jié)。HEAD請求先確認遠程文件大小避免每次重復下載這個判斷在批量運行時能省下大把時間。斷點續(xù)傳用的是Range: bytesresume-服務端返回206 Partial Content時文件用追加模式寫進去。重試時用了指數(shù)退避而不是固定間隔因為官網(wǎng)有時會對頻繁請求做短期限流退避能明顯提高成功率。3.3 合并與導出SEQN 關聯(lián)和 Parquet 落地NHANES 所有數(shù)據(jù)表都帶一個受訪者編號列SEQN這是合并的核心鍵。以人口學數(shù)據(jù)和膳食數(shù)據(jù)為例import pandas as pd def load_xpt(path: str, usecolsNone) - pd.DataFrame: return pd.read_sas(path, formatxport, encodingutf-8, usecolsusecols) demo load_xpt(P_DEMO.xpt, usecols[SEQN, RIAGENDR, RIDAGEYR, RIDRETH1]) diet load_xpt(P_DR1TOT.xpt, usecols[SEQN, DR1TKCAL, DR1TPROT, DR1TCARB]) merged pd.merge(demo, diet, onSEQN, howleft) merged.to_parquet(merged_demo_diet.parquet, indexFalse)usecols參數(shù)非常關鍵。P_DEMO.xpt表里其實有幾十個變量但很多課題根本用不上如果全部讀入內(nèi)存合并幾個大表很容易把 16G 內(nèi)存跑滿。只選需要的列加載速度和內(nèi)存占用都能降一個量級。導出我默認用 Parquet 而不是 CSV原因有三個列類型信息不會丟失CSV 會把年齡、體重全部轉(zhuǎn)成字符串下次讀入又要重新指定類型、文件體積比 CSV 小很多、讀取速度快。如果你后續(xù)要用 Stata 或 SAS也可以改成to_stata()或to_csv()pandas 都有現(xiàn)成接口。4. 整理環(huán)節(jié)真正磨人的地方變量、缺失值、跨周期4.1 變量命名的慣用前綴看懂它等于看懂半個 codebookNHANES 的變量名不是亂起的大部分都有前綴規(guī)律。比如LBX開頭的是實驗室檢測指標BMX開頭的是體檢指標MCQ開頭的是醫(yī)療條件問卷DRX開頭的是膳食回憶。我看到LBXHDL基本能猜到是高密度脂蛋白膽固醇的實驗室結果看到BMXBMI能猜到是體檢測的 BMI。這個規(guī)律看起來簡單但能幫你少翻很多次 codebook 網(wǎng)頁。我建議下載完數(shù)據(jù)后先把每個表的列名、標簽、數(shù)據(jù)類型導成一份 CSV 保存下來做分析時隨時查。運行下面這段代碼就能在當前目錄生成所有表的變量字典import pandas as pd from pathlib import Path records [] for xpt in Path(data).glob(*.xpt): df pd.read_sas(xpt, formatxport, encodingutf-8) for col in df.columns: records.append({file: xpt.name, variable: col, dtype: str(df[col].dtype)}) pd.DataFrame(records).to_csv(variable_dict.csv, indexFalse)4.2 缺失值編碼7777、9999 和點號這是新手最容易翻車的地方。NHANES 的缺失值并不統(tǒng)一有一部分缺失值是 SAS 里的點號.讀入 pandas 后變成NaN這部分處理起來很順手但有一部分是編碼型缺失比如問卷調(diào)查里“不知道”填9999、“拒絕回答”填7777、“不適用”填8888這類值讀進來是正常數(shù)字直接拿來算均值結果會離譜到讓你懷疑數(shù)據(jù)源壞了。我在整理時對每個數(shù)值型字段都會先看一眼取值分布。比如某個變量min 0, max 9999那基本可以確定 9999 是編碼缺失需要提前過濾或標記。這個檢查不能靠猜最終依據(jù)是官網(wǎng)每個數(shù)據(jù)文件頁面附帶的 Codebook里面會詳細列出每個編碼的含義。我的工具里沒有寫死這套邏輯因為不同變量差異太大只能靠人工確認后配置。4.3 跨周期合并的大坑多周期合并是 NHANES 使用中最常見也最隱蔽的坑。不同周期的同一個變量名字可能相同但編碼含義可能不同更麻煩的是某些變量只在部分周期存在直接pd.concat會得到滿屏的 NaN。我的建議是先按周期分別清洗再縱向合并而不是先合并再清洗。每個周期獨立讀入、獨立過濾缺失值、獨立重命名最后用pd.concat(..., ignore_indexTrue)拼成一個大文件。這樣每一期的處理邏輯都能自查出了問題也容易定位。另外合并前用pd.merge按SEQN做橫向關聯(lián)時要確認兩邊的SEQN類型一致。XPT 讀進來時SEQN有時是浮點有時是字符串不一致會導致合并結果全是 NaN。統(tǒng)一轉(zhuǎn)成 int64 再 merge能避免這種低級錯誤。5. 實測記錄下載、斷點、合并的完整結果5.1 一次真實運行過程我用 2017-2020 周期的 Demographics、Dietary 兩個組件做了完整測試。官網(wǎng)目錄頁解析用了約 5 秒識別出來 8 個 XPT 直鏈其中 4 個是本次需要的其余幾個是重復入口或不需要的補充文件。下載 4 個文件合計約 120MB耗時 58 秒中途我故意殺掉進程模擬斷線重新運行時已有文件瞬間跳過斷掉的那個從頭尾接著下完全符合預期。合并階段先用usecols只讀入需要的列人口學表 9000 多行膳食表 9000 多行按SEQN左連接后行數(shù)不變等于 9500 左右說明每個受訪者都有膳食記錄沒有因為連接把樣本丟掉。導出 Parquet 文件耗時不到 3 秒體積只有 CSV 的三分之一。步驟耗時文件量說明解析目錄5s8 個鏈接去重后為 8下載 4 個 XPT58s約 120MB模擬斷線后續(xù)傳成功讀取與合并6s2 表 / 9500 行按 SEQN 左連接導出 Parquet3s1 個文件是 CSV 體積的 1/35.2 遇到的兩個典型異常及處理第一次跑工具時下載到第三個文件直接報403 Forbidden。排查發(fā)現(xiàn)是requests默認的User-Agent被服務器拒絕了加上Mozilla/5.0之后恢復正常。這個問題在批量抓取場景里很常見不只是 NHANES遇到 403 先檢查請求頭。第二次遇到的是官網(wǎng)偶發(fā)的503 Service Unavailable。這是典型的臨時性錯誤重試就能解決。我的代碼里退避策略是2 ** attempt秒最多三次。實測下來一次 503 大約在第 2 次重試后成功沒有出現(xiàn)連續(xù)失敗的情況。如果你在特定時段比如北美中午下載容易撞上高負載可以把最大重試次數(shù)調(diào)到 5但間隔也要相應拉長別把服務器打崩。6. 延伸從下載工具到你的研究數(shù)據(jù)管線6.1 與 R / SAS / Stata 對接整套工具跑完后本地數(shù)據(jù)倉庫是干凈的 Parquet 或 CSV 文件。后續(xù)用 R 做統(tǒng)計模型直接read_parquet()導入用 SAS 就導出to_sas()的 XPT 格式用 Stata 則to_stata()。因為整理階段已經(jīng)把缺失值編碼、變量重命名、跨周期合并處理完了進入統(tǒng)計軟件時基本就是可以直接建模的干凈數(shù)據(jù)框能省掉大量重復勞動。6.2 建議長期維護的幾個東西如果你打算把 NHANES 數(shù)據(jù)作為長期分析資源建議維護三樣東西一個README.md記錄每個文件的下載日期、遠程鏈接和本地路徑一個variable_dict.csv記錄變量名與標簽映射一個processed/目錄專門放清洗后的最終數(shù)據(jù)集。這樣過三個月再回來你還能清楚地知道手里的數(shù)據(jù)是從哪個周期來的、清洗邏輯是什么。我自己把下載腳本封裝成命令行工具參數(shù)化組件、周期、輸出目錄配合計劃任務定期檢查官網(wǎng)有沒有更新。6.3 一點個人體會NHANES 下載整理這件事技術難度不高但它足夠繁瑣繁瑣到值得寫一套自動化工具。做完之后最大的收獲不是省下那一周的下載時間而是從此你擁有了一條可復用的數(shù)據(jù)流水線換一個周期、換一批變量改個參數(shù)就能跑通。以后再接觸類似的大型公開數(shù)據(jù)庫比如行為風險因素監(jiān)測系統(tǒng)、醫(yī)療支出面板調(diào)查這套“解析頁面-斷點下載-變量字典-橫向合并-縱向拼接”的思路可以原樣遷移。最后提醒一句官網(wǎng)頁面結構偶爾會調(diào)整解析函數(shù)要定期跑一次確認。我的經(jīng)驗是每年檢查兩次每次改版后花十分鐘更新選擇器就行別等到跑批時才發(fā)現(xiàn)鏈接全解析不出來了。數(shù)據(jù)整理這種事前期把地基打穩(wěn)后面分析才能睡得著覺。本文還有配套的精品資源點擊獲取