Author: Kyle Grealis, MS¹ · Natalie Neugaard (Goulett), MPH · Amrit Baral, PhD, MBBS, MPH² · Raymond Balise, PhD¹ · Johannes Thrul, PhD² · Janardan Devkota, PhD² ¹ Dept. of Biostatistics, University of Miami, Miller School of Medicine, Miami, FL ² Dept. of Mental Health, Bloomberg School of Public Health, Johns Hopkins University, Baltimore, MD
Date: 2026-04-23

# nhanesdata

Harmonized, cloud-hosted NHANES data for streamlined research workflows

Kyle Grealis, MS¹ · Natalie Neugaard (Goulett), MPH · Amrit Baral, PhD, MBBS, MPH² · Raymond Balise, PhD¹ · Johannes Thrul, PhD² · Janardan Devkota, PhD²
¹ Dept. of Biostatistics, University of Miami, Miller School of Medicine, Miami, FL
² Dept. of Mental Health, Bloomberg School of Public Health, Johns Hopkins University, Baltimore, MD

April 23, 2026

Good [morning/afternoon] everyone. My name is Natalie Neugaard, I’m a researcher at the University of Miami. Today I want to introduce nhanesdata — a package our team built to make working with NHANES data faster, more consistent, and more reproducible. I’ll walk you through why we built it, how it works, and show some examples of it in use.

# Background

Let me start with a little background on NHANES for those who may be less familiar.

## What is NHANES?

The National Health and Nutrition Examination Survey (NHANES) is:

- A nationally representative, complex survey of U.S. civilians
- Continuous since 1999, collected in 2-year cycles
- Combines interviews, physical exams, and laboratory tests
- A cornerstone dataset for population health research

Data spans:

Category | Datasets
--- | ---
Laboratory | 173
Questionnaire | 77
Examination | 75
Dietary | 14
Total | 339+

> NHANES is powerful, but working with it at scale is painful.

1. The CDC’s National Health and Nutrition Examination Survey, or NHANES, is one of the most valuable public health datasets we have. It’s nationally representative, it has decades of data, and it’s free.
2. The catch is that its datasets come in two-year cycle chunks, and combining all of them can get painful.

## The Problem

The CDC publishes data in two-year cycle fragments with cryptic naming:

    DEMO    DEMO_B    DEMO_C    DEMO_D    ...    DEMO_L
    (1999)  (2001)    (2003)    (2005)           (2021)

Pain points for researchers:

- CDC servers are slow and frequently unreliable
- Every table must be individually downloaded, cleaned, and merged
- Column types conflict across cycles (numeric in one cycle, factor in another)
- Inconsistent variable coding: raw numeric codes, not labels
- Repetitive, error-prone boilerplate code in every new project

> For large data science projects and iterative workflows, this becomes a real bottleneck.

1. When you go to download NHANES data, you’re looking at 13 separate sets of files per table.
2. The CDC servers are slow and go down regularly. Each file needs to be downloaded, cleaned, and merged. Column types don’t always agree across cycles, and variables are often stored as raw numeric codes rather than meaningful labels.
3. For a one-off project you can get through it — but in iterative workflows, this overhead becomes a real drag.

# The Solution

So — what did we do about it? That’s where nhanesdata comes in.

## nhanesdata: What We Built

Built on the shoulders of the excellent `nhanesA` package (Endres et al.), which made NHANES data more accessible, but still relied on CDC’s servers.

We added:

- Pre-merged tables across all cycles
- Harmonized column types and labels
- Parquet files hosted on a public cloud server (Cloudflare R2)
- `create_design()` for CDC-compliant survey weighting
- Variable and term search utilities

The result:

- Near-instantaneous downloads
- No CDC server dependency at runtime
- Reproducible, consistent data ready to join and analyze
- Lower barrier to entry for NHANES research

We built nhanesdata on top of nhanesA, which already made NHANES much more accessible in R. Our contribution was to take that a step further: we downloaded all the data, merged it across cycles, harmonized the types and labels, and stored it as Parquet files on a public cloud server. Parquet is a compressed, columnar file format that loads extremely quickly — that’s what allows near-instantaneous downloads without depending on CDC’s servers at all.

## How It Works

Instead of downloading data by cycle, `nhanesdata` serves it by table. All cycles for a given table are already merged into a single dataset.

For example, `read_nhanes("demo")` returns one tibble containing NHANES demographics from 1999 through 2023 — every cycle combined, ready to use.

Every dataset always contains two key variables:

- `seqn`: the participant ID
- `year`: the survey cycle start year

1. Instead of dealing with 13 separate files per table, one function call returns a single merged tibble. For example, the function `read_nhanes("demo")` returns one tibble containing all participant demographics for each cycle from 1999 through 2023.
2. Every dataset always includes seqn and year, which are the two keys you need for everything downstream.

## Data Harmonization: The Problem

`nhanesA` translates numeric codes to labeled factors only when a parseable CDC codebook exists for that cycle. When one doesn’t, the same variable comes back as raw numbers.

Same variable, two cycles:

    Cycle with codebook:    BMIWT = "Could not obtain" / "Clothing" / "Medical appliance"
    Cycle without:          BMIWT = 1 / 3 / 4

Add R type conflicts (`integer` vs `double`, `factor` vs `character`) and variables absent from certain cycles, and there is a lot to resolve before `bind_rows()` will even run.

1. The biggest challenge is harmonization. nhanesA uses CDC codebooks to translate numeric codes into labeled factors — so instead of a 1, you get “Male”. But not every cycle has a parseable codebook. When one doesn’t, the same variable comes back as raw numbers. So a single variable across 13 cycles might be labeled in 10 and numeric in 3.
2. On top of that, conflicts between R classes and variables absent from certain cycles, and there’s a lot to sort out.

## Data Harmonization: Our Approach

What we did:

- Cached translation tables from cycles that do have codebooks and applied them to cycles that don’t
- Resolved all R type conflicts across cycles before binding
- Variables not collected in a given cycle are filled with `NA`, not dropped

Warning

We’re human. We did our best. If a variable is critical to your analysis, verify it against the CDC codebook. Use `get_url("DEMO_J")` for a direct link.

1. Our solution was to cache the translation tables from cycles that do have codebooks, then apply those mappings to cycles that don’t. We resolved all the R type conflicts before binding, and filled missing variables with NA rather than dropping them.
2. That being said — this is a labor-intensive process and we can’t guarantee perfection. Verify variables using the `get_url()` function, which provides a direct link to a table’s page in the CDC codebook.

# Using nhanesdata

Now let’s look at what using the package actually looks like in practice.

## Installation & Loading Data

    install.packages("nhanesdata")
    library(nhanesdata)

    # Load any NHANES table - merged across all cycles, instantly
    demo  <- read_nhanes("demo")   # Demographics, 1999-2023
    bpx   <- read_nhanes("bpx")    # Blood pressure
    bmx   <- read_nhanes("bmx")    # Body measures
    tchol <- read_nhanes("tchol")  # Total cholesterol

    # Not sure what a table is called? Search by keyword or variable name
    term_search("blood pressure")   # keyword search to find table name
    var_search("BPXSY1")            # find which cycles contain a variable
    get_url("BPX_J")                # open CDC codebook for a specific cycle

1. The package is on CRAN, so installation is a single line. Once loaded, you call read_nhanes with the table name — it’s not case-sensitive, and the download takes just a few seconds. Here we’re loading demographics, blood pressure, body measures, and total cholesterol.
2. If you’re not sure what a table is called, the package includes three search helpers. term_search takes a keyword and returns matching table names. var_search takes a specific variable name and tells you which cycles contain it. And get_url returns a direct link to that table’s CDC codebook page.

## Joining & Filtering

    library(nhanesdata)
    library(dplyr)

    demo  <- read_nhanes("demo")
    bpx   <- read_nhanes("bpx")
    bmx   <- read_nhanes("bmx")

    # Join on BOTH seqn AND year - seqn alone is not unique across cycles
    analysis <- demo |>
      inner_join(bpx, by = c("seqn", "year")) |>
      inner_join(bmx, by = c("seqn", "year")) |>
      filter(
        ridageyr >= 18,       # adults only
        year >= 2007,         # 2007-2017 cycles
        year <= 2017
      ) |>
      mutate(obese = bmxbmi >= 30)

> Filtering and variable creation should happen before creating the survey design object.

1. One thing to flag here: seqn is only unique within a cycle, not across the full dataset. Joining on seqn alone will silently produce wrong results by matching participants from different cycles who happen to share an ID. Always join on both seqn and year.

2. Then filter and create any derived variables you need before creating the survey design object — modifying the data afterward can break the weight calculations.

## Survey Design

NHANES oversamples certain subgroups, so each participant carries a sampling weight. Ignoring weights produces biased population estimates.

Pick the weight type that matches your most restrictive data source:

Weight | Use when your analysis includes…
--- | ---
`"interview"` | Questionnaire / demographics only
`"mec"` | Any physical exam or lab data
`"fasting"` | Fasting lab measurements

    design <- create_design(
      dsn      = analysis,
      start_yr = 2007,
      end_yr   = 2017,
      wt_type  = "mec"
    )

`create_design()` handles CDC-required weight scaling across cycles automatically.

1. NHANES oversamples certain groups — elderly, minorities, low-income — so unweighted estimates are biased. Every participant carries a sampling weight representing how many people they stand for in the U.S. population. The key decision is weight type: use interview for questionnaire data only, mec if your analysis involves any exam or lab data, and fasting for fasting labs. When in doubt, use the most restrictive option for what you’re analyzing.

2. The `create_design` function handles all the CDC-required weight scaling behind the scenes — including the tricky adjustments for combining multiple cycles.

## Survey Design: Weighted Analysis

    library(srvyr)

    # Weighted mean systolic BP and obesity prevalence by age group
    design |>
      group_by(age_60plus = ridageyr >= 60) |>
      summarize(
        mean_sbp  = survey_mean(bpxsy1, na.rm = TRUE),
        pct_obese = survey_mean(obese,  na.rm = TRUE),
        n         = survey_total(vartype = NULL)
      )

    # A tibble: 2 × 6
      age_60plus mean_sbp mean_sbp_se pct_obese pct_obese_se        n
      <lgl>         <dbl>       <dbl>     <dbl>        <dbl>    <dbl>
    1 FALSE          119.       0.315     0.332       0.00531 178456823
    2 TRUE           135.       0.481     0.357       0.00902  41234109

1. Once you have the design object, you use srvyr verbs just like dplyr — group_by, summarize — but estimates are properly weighted for the U.S. population.

2. The n column here represents estimated population counts, not sample sizes, which is a useful reminder of what weighted NHANES analysis actually gives you.

## Mortality Linkage

One of the most powerful features: `read_nhanes("mortality")`

    library(survival)

    demo      <- read_nhanes("demo")
    mortality <- read_nhanes("mortality")   # NDI linkage through Dec 31, 2019

    survival_data <- demo |>
      inner_join(mortality, by = c("seqn", "year")) |>
      filter(ridageyr >= 40, year >= 1999)

    design <- create_design(survival_data, 1999, 2017, wt_type = "interview")

    svycoxph(
      Surv(permth_exm / 12, mortstat == 1) ~ ridageyr + riagendr,
      design = design
    )

> Population-representative survival analyses, entirely in R.

1. I want to highlight the mortality linkage dataset specifically. read_nhanes(“mortality”) gives you NDI-linked mortality data through the end of 2019. Combined with the `create_design` function,
2. you can run survey-weighted survival models — like the Cox proportional hazards model shown on line 12, without ever leaving R or touching the CDC website. This is especially useful for chronic disease and aging research.

# Caveats & What’s Next

Before wrapping up, I want to be upfront about what the package does not yet cover.

## What’s Not Included (Yet)

We harmonized the majority of NHANES datasets, but some are excluded:

Currently excluded:

- Datasets where CDC suffix conventions are not followed
- Datasets with repeated measures (require special handling)
- Surplus, pooled, and restricted-access samples
- The 2019-2020 cycle (COVID-19 disrupted data collection; CDC advises against combining it with standard cycles)

We’re actively working on expanding coverage. If you need a specific dataset not yet available, open a GitHub issue or contribute!

1. A few important caveats. We haven’t yet harmonized datasets where CDC doesn’t follow the standard suffix naming conventions, or datasets with repeated measures, which need special handling. The 2019-2020 cycle is intentionally excluded — CDC itself advises against combining it with standard cycles because of COVID-19 disruption to data collection.
2. We’re actively expanding coverage, and if there’s a specific dataset you need, we welcome GitHub issues or contributions.

## The Bottom Line

Goal: Abstract away the data science overhead so researchers can focus on their science.

We achieved this by:

- Harmonizing 339+ datasets across 13 cycles
- Hosting on a public, no-auth cloud server
- Serving as Parquet files for near-instant access
- Providing `create_design()` for CDC-compliant weighting

What it means for you:

- Go from install to analysis in minutes
- Consistent, documented data you can trust
- Reproducible workflows that don’t break when CDC servers go down

The core goal is accessibility. We wanted to remove the data science overhead so researchers can focus on the science. On the left is how we got there: over 300 harmonized datasets merged across 13 cycles, hosted publicly as Parquet files, with a design object function that handles the CDC weighting rules for you. On the right is what that means in practice: you can go from install to a weighted analysis in a few lines of code, with data that is consistent across cycles and does not depend on CDC’s servers being up. We used this package for our own research workflows, and we are now releasing it publicly so others can benefit from the same foundation.

## Thank You

Get started:

    install.packages("nhanesdata")

Resources:

- 📦 github.com/kyleGrealis/nhanesdata
- 📖 Package website & vignettes
  - Getting Started
  - Creating Survey Design Objects
  - Available Datasets
  - COVID-19 and the 2019-2020 Cycle

Built on: `nhanesA`, `arrow`, `srvyr`, `dplyr`

Contributors:

- Kyle Grealis, MS (lead developer)
- Natalie Neugaard (Goulett), MPH (presenter)
- Amrit Baral, PhD, MBBS, MPH
- Raymond Balise, PhD
- Johannes Thrul, PhD
- Janardan Devkota, PhD

The package is on CRAN and on GitHub, and there’s a full documentation site with vignettes covering everything we touched on today — getting started, survey design, the full dataset catalog, and the reasoning behind excluding the COVID cycle. The link in the footer will take you straight to the GitHub repo. Thank you for your time — I’m happy to take any questions.