from urllib.request import urlretrieve
urlretrieve(
"https://wwwn.cdc.gov/Nchs/Data/Nhanes/Public/2021/DataFiles/DEMO_L.xpt",
"DEMO_L.xpt",
)Read NHANES DEMO_L.xpt in Python
DEMO_L.xpt, NHANES DEMO_L, read xpt file Python, NHANES Python, SAS transport file, NHANES survey weights, complex survey analysis Python
DEMO_L.xpt is the demographics file of the NHANES August 2021–August 2023 cycle. It holds one row per participant (11,933 people) with age, sex, race and ethnicity, income, the survey weights and the design variables. Every other module of the cycle joins onto it by SEQN.
The .xpt extension means SAS transport format. You do not need SAS to read it. With svy it takes one line.
Get the file
CDC publishes it on the NHANES 2021–2023 demographics page. Download it once:
Read it
pip install svysvy.read_xpt returns a polars DataFrame:
import svy
demo = svy.read_xpt("DEMO_L.xpt")
print(demo.shape)
demo.select("SEQN", "RIAGENDR", "RIDAGEYR", "SDMVSTRA", "SDMVPSU", "WTINT2YR", "WTMEC2YR").head(3)(11933, 27)
| SEQN | RIAGENDR | RIDAGEYR | SDMVSTRA | SDMVPSU | WTINT2YR | WTMEC2YR |
|---|---|---|---|---|---|---|
| f64 | f64 | f64 | f64 | f64 | f64 | f64 |
| 130378.0 | 1.0 | 43.0 | 173.0 | 2.0 | 50055.450807 | 54374.463898 |
| 130379.0 | 1.0 | 66.0 | 173.0 | 2.0 | 29087.450605 | 34084.721548 |
| 130380.0 | 2.0 | 44.0 | 174.0 | 1.0 | 80062.674301 | 81196.277992 |
Pass columns= to read only what you need:
svy.read_xpt("DEMO_L.xpt", columns=["SEQN", "RIDAGEYR"]).shape(11933, 2)
svy.read_sas is the same reader. It also opens SAS .sas7bdat datasets.
Keep the labels
Variable names like RIDAGEYR are hard to read. The file carries a label for each one. svy.create_from_xpt reads the data and the labels together into a Sample:
sample = svy.create_from_xpt("DEMO_L.xpt")
for var in ["RIAGENDR", "RIDAGEYR", "INDFMPIR", "WTMEC2YR"]:
print(f"{var:9} {sample.meta.var_labels[var]}")RIAGENDR Gender
RIDAGEYR Age in years at screening
INDFMPIR Ratio of family income to poverty
WTMEC2YR Full sample 2-year MEC exam weight
Value labels are another matter. The XPT file does not contain them, so RIAGENDR arrives as 1 and 2. The codes are in the DEMO_L codebook. Add the ones you need:
sample = sample.set_value_labels("RIAGENDR", {1: "Male", 2: "Female"})Set the design and the weight
NHANES is a stratified, clustered sample, so plain means and counts are wrong. The design uses the masked strata and PSUs from this file:
sample = sample.set_design(
svy.Design(stratum="SDMVSTRA", psu="SDMVPSU", wgt="WTINT2YR")
)
print(sample.n_strata, "strata,", sample.n_psus, "PSUs")15 strata, 30 PSUs
The file has two weights, and choosing the wrong one is easy:
WTINT2YRis for variables from the interview, which everyone did.WTMEC2YRis for variables from the exam at the mobile examination centre (MEC). It is zero for the people who were only interviewed:
print("exam weight > 0:", (demo["WTMEC2YR"] > 0).sum())
print("exam weight = 0:", (demo["WTMEC2YR"] == 0).sum())exam weight > 0: 8860
exam weight = 0: 3073
Here everything comes from the interview, so WTINT2YR is the right weight. Estimates now carry design-based standard errors:
print(sample.estimation.prop("RIAGENDR"))╭─────────────── Estimate: PROP (TAYLOR) ───────────────╮ │ │ │ Gender est se lci uci cv (%) │ │ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ │ │ Female 0.5085 0.0068 0.4941 0.5230 1.33 │ │ Male 0.4915 0.0068 0.4770 0.5059 1.38 │ │ │ ╰───────────────────────────────────────────────────────╯
print(sample.estimation.mean("RIDAGEYR"))╭──────────── Estimate: MEAN (TAYLOR) ────────────╮ │ y: Age in years at screening │ │ │ │ est se lci uci cv (%) │ │ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ │ │ 38.9893 0.5149 37.8918 40.0868 1.32 │ │ │ ╰─────────────────────────────────────────────────╯
To estimate for a subgroup, say adults, use where=. Do not filter rows first, because that understates the variance:
print(sample.estimation.mean("RIDAGEYR", where=svy.col("RIDAGEYR") >= 20))╭──────────── Estimate: MEAN (TAYLOR) ────────────╮ │ y: Age in years at screening │ │ where: RIDAGEYR >= 20 │ │ │ │ est se lci uci cv (%) │ │ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ │ │ 48.7565 0.3993 47.9053 49.6076 0.82 │ │ │ ╰─────────────────────────────────────────────────╯
Next
DEMO_L.xpt alone answers demographic questions. For health outcomes, join the questionnaire, exam and lab modules onto it by SEQN. Then deal with the refusal codes, the skip patterns and the choice of weight, which matter more than the estimator.
The case study svy on NHANES, August 2021–August 2023 does all of it on eight modules and reproduces published NCHS estimates at published precision.
Other formats
The same steps work for the other two formats. Only the reader changes, along with what each format keeps:
- Stata: Read Stata .dta files in Python: MEPS 2023. Value labels come through, but reserve codes are ordinary numbers.
- SPSS: Read SPSS .sav files in Python: GSS 2024. Declared missing values arrive as nulls.