README.md next to this file is the reference: how to run
it, what lives where, and the caveats. This page is the
tour — what each script decides, what it writes, and the two
places where NHANES will quietly give you the wrong answer.
Read it before you write P1. Your own paper will not be this one, but the shape of the work is the same: rebuild the cohort, count what you lose at each step, then analyse.
source("R/run_all.R")The project root is detected automatically, so the folder can sit
anywhere. Internet is needed only by 01_download.R, and
only if data/raw/ is not already populated.
Every step writes a log to logs/. When something
disagrees with the paper, the log for that step is the first place to
look.
Cohort construction is the analysis in a replication like
this one. Almost every discrepancy you will chase traces back to a row
here, so 02_build_analytic.R writes it out as a file —
logs/sample_funnel.csv:
| Step | N | Type | Dropped |
|---|---|---|---|
| All NHANES 2007–2018 records | 59,842 | — | — |
| Age ≥ 18 | 36,580 | ELIGIBILITY | 23,262 |
| In fasting subsample | 14,962 | DESIGN | 21,618 |
| FLI computable (TG + GGT + BMI + waist) | 14,170 | MISSINGNESS | 792 |
| Steatosis (FLI ≥ 60) | 6,052 | TARGET POPULATION | 8,118 |
| MASLD (≥ 1 cardiometabolic criterion) | 6,050 | TARGET POPULATION | 2 |
| Phenotype group assignable | 6,050 | MISSINGNESS | 0 |
| Mortality-eligible and linked | 6,048 | OUTCOME ASCERTAINMENT | 2 |
Every step carries a type label, because a funnel shows N falling and does not show why — and the reasons are not interchangeable. After eligibility, the two largest reductions are the fasting subsample (DESIGN) and hepatic steatosis (TARGET POPULATION), and neither is missing data. Genuine missingness is 792 item-nonresponse losses, and separately 2 records unlinked to mortality follow-up. Those are different problems: they are not added together, and neither of them is 30,532, which is what reading every post-age-eligibility reduction as attrition would give.
The fasting-subsample row is the one most often mistyped. Only a
planned subset of participants were ever asked to fast, and
WTSAF2YR exists to account for that selection stage and its
nonresponse, so absence of fasting measures outside the subsample is a
design feature and is not, by itself, a missing-data
problem. Item nonresponse within the subsample is the separate
thing, and it is 792 people. That is the first trap, below.
| Row set | N | What it is |
|---|---|---|
| Full analytic file | 6,371 | the funnel above with the DESIGN row removed: every adult with a computable FLI, whether or not they carry a valid fasting weight |
| Locked domain | 6,048 | the funnel above as written, ending at records that carry a valid
WTSAF2YR — the only set on which a design-aware estimate is
possible |
These are the same two row sets the worked examples name. They differ
by 323 records, and the difference is not an error in either. The paper
is unweighted, so the full analytic file is the one that
reproduces it: Table 1, Table 2 and the figures in
output/ are built on 6,371 against the paper’s
approximately 6,300 (Table 2’s Model 1 and Model 2 columns use the
complete cases among those rows). Its own FLI step is the 14,989 that
logs/sample_funnel.csv records, and 14,989 exceeds the
14,962 adults in the fasting-weight frame because 819 adults have all
four FLI inputs without carrying a valid WTSAF2YR. The
locked domain is a different — and, for a design-aware estimate, the
correct — denominator, not a correction to the first.
Getting 6,371 rather than the paper’s number is not a failure — it is the finding, and tracing why the two differ is the skill being taught. An unexplained exact match is worth less than a discrepancy you can account for.
One line of logs/sample_funnel.csv records a switch
rather than a filter: Alcohol exclusion applied = FALSE
closes the file, and it is the reason the full analytic file stays at
6,371 rather than dropping further. See the alcohol finding in
reproduction_report.md.
| Script | What it decides | What it writes |
|---|---|---|
00_setup.R |
Project root, the output folder paths, the six NHANES cycles, and
the list of data components to download — shared constants only. It
defines no analysis switch:
APPLY_ALCOHOL_EXCL is defined in
02_build_analytic.R, which is also where the funnel log
records whether it was applied |
nothing |
01_download.R |
Nothing analytic. Fetches raw NHANES cycles and the linked mortality files, untouched | data/raw/<cycle>/,
data/raw/mortality/, and
_download_manifest.csv recording URL, size and status per
file |
02_build_analytic.R |
Everything that matters. Merges cycles, computes
FLI → steatosis, separates the fasting-subsample step (DESIGN) from FLI
item nonresponse (MISSINGNESS), applies the MASLD criteria, handles
alcohol, assigns the four BMI/WHtR groups, links mortality, and marks
the locked domain using WTSAF2YR |
the analytic dataset and logs/sample_funnel.csv |
03_table1.R |
Baseline characteristics by group — mean (SD) for age, median (IQR) for skewed measures, n (%) for categorical; ANOVA / Kruskal–Wallis / chi-square | output/tables/table1_reproduced.{csv,html} |
04_table2_cox.R |
Cox models for all-cause and CV death, Group I as reference: unadjusted, Model 1, Model 2 | output/tables/table2_reproduced.{csv,html} |
05_figure1_km.R |
Kaplan–Meier by group, with number-at-risk and log-rank | output/figures/figure1_km.png |
06_figure2_rcs.R |
Restricted cubic splines (4 knots) for BMI and WHtR against probability of death | output/figures/figure2_rcs.png |
The definitions worth knowing before you read the code: steatosis is FLI ≥ 60; MASLD is steatosis plus at least one cardiometabolic criterion; obesity is BMI ≥ 30 (≥ 25 for Asian participants); central adiposity is WHtR ≥ 0.6. Those four combine into Groups I–IV.
Both of these are in 02_build_analytic.R. Both cost real
time to find. Neither is specific to this paper — you will meet them
again.
WTSAF2YR is a designed subsample, not missing dataTriglycerides come from the fasting subsample, which
carries its own weight, WTSAF2YR. Only a planned subset of
participants were ever asked to fast, so those missing values are
missing by design.
This matters twice over. It fixes where the drop belongs in the
funnel: the 21,618 people lost at In fasting subsample
were never sampled for the measurement, not lost to it, and they are a
different problem from the 792 lost at FLI computable,
who were sampled and still lack an input. Collapsing the two into a
single “FLI computable” step is the error the typed funnel above exists
to prevent. And it changes how a design-aware analysis has to be built:
you construct the survey design on everyone with a valid fasting
weight, not on the MASLD cohort, and then subset()
to the analysis domain, as the P4 exemplar does. Filtering the data to
the analysis cohort before building the design discards the
information the variance estimator needs, and the standard errors can
come out wrong.
This pipeline uses the variable for one thing only: separating those two funnel steps and marking the locked domain. It never weights with it. Doing something design-aware with it is P4’s job, which is why P4 exists as a separate increment.
ALQ130 has reserved codes, and is the wrong variable on its
ownALQ130 is drinks per drinking-day, not
consumption. Significant alcohol intake needs frequency ×
quantity — ALQ120Q / ALQ120U combined
with ALQ130. Using ALQ130 alone removes 2,063
people from the full analytic file, where frequency × quantity removes
427, so it over-excludes about 1,600.
Worse, both variables use 777 and 999 as reserved
codes for “refused” and “don’t know”. Leave them in and a
refusal becomes someone reporting 777 drinks — reclassifying nine people
who refused or answered “don’t know” as heavy drinkers. They must be set
to NA before any threshold rule is applied. The code does
this for both variables before combining them.
The general lesson: in NHANES, check the codebook for reserved values before you compare a variable to a number. A silent misclassification of nine people is exactly the kind of error a replication is supposed to catch.
04_table2_cox.R uses coxph, not
svycoxph. There is no survey design object anywhere in
03–06.
That is deliberate: this pipeline reproduces the paper’s analysis, and the paper’s analysis is unweighted. Reproducing a choice is not endorsing it. The design-aware estimate is the interrogation half of the course, and it lands in P4 and M1 — where the question becomes what the weighted answer is and whether it changes the conclusion.
Keep the two apart in your own work as well. Replicate first, faithfully, and only then say what you would have done differently.
In order:
logs/sample_funnel.csv — is the
disagreement in the cohort, before any modelling? Most are.logs/ — each script
writes one.reproduction_report.md — what
reproduced, what did not, and why, already written up for this
paper.Then write down what you found. A traced discrepancy is a result; a silent one is a bug.