Final Project

The Relationship Between Age and Nutrient Consumption

Introduction and Background

(CDCa)

My dataset is from the Centers for Disease Control and Prevention’s (CDC) National Health and Nutrition Examination Survey (NHANES), 2021-2023. NHANES data is collected yearly from 5000 Americans meant to represent the diverse American population (CDC, 2024a; CDC, 2024b). NHANES team members conduct in person interviews with American households on health and food intake, and participants receive a health examination at a mobile clinic. The CDC intentionally over samples populations with differing health outcomes, such as children and African Americans (CDC, 2024c). The data is then weighted during analysis to match the demographic makeup of the country.

For my analysis, I will use the age variable from the demographic data and sodium, caffeine, cholesterol, and fiber intake variables from the day 1 food intake interview data. I plan to explore if intake of these nutrients can predict someone’s age. This was inspired by a brief published using the NHANES data that found that caffeine consumption increased with age in children under 19 (Young & Branum, 2026). I was interested to see if these relationships between nutrient consumption and age also existed in adults and with more than just caffeine. Additionally, many credible organizations, such as the American Heart Association, recommend that older Americans change their nutrient intake to support the health concerns of aging, so it seemed reasonable to me that nutrient intake and age could have a linear relationship (Williamson, 2024).

R Setup

# Loading tidyverse.
library(tidyverse)
Warning: package 'stringr' was built under R version 4.6.1
── Attaching core tidyverse packages ──────────────────────── tidyverse 2.0.0 ──
✔ dplyr     1.2.1     ✔ readr     2.2.0
✔ forcats   1.0.1     ✔ stringr   1.6.0
✔ ggplot2   4.0.3     ✔ tibble    3.3.1
✔ lubridate 1.9.5     ✔ tidyr     1.3.2
✔ purrr     1.2.2     
── Conflicts ────────────────────────────────────────── tidyverse_conflicts() ──
✖ dplyr::filter() masks stats::filter()
✖ dplyr::lag()    masks stats::lag()
ℹ Use the conflicted package (<http://conflicted.r-lib.org/>) to force all conflicts to become errors
# Loading ggfortify for diagnostic plots.
library(ggfortify)
Warning: package 'ggfortify' was built under R version 4.6.1
# Loading plotly for interactive plots.
library(plotly)
Warning: package 'plotly' was built under R version 4.6.1

Attaching package: 'plotly'

The following object is masked from 'package:ggplot2':

    last_plot

The following object is masked from 'package:stats':

    filter

The following object is masked from 'package:graphics':

    layout
# Loading my data. Note that I have two datasets with the same respondents that I'll need to combine.
totals <- read_csv("nhanes_day_one_totals.csv")
Warning: One or more parsing issues, call `problems()` on your data frame for details,
e.g.:
  dat <- vroom(...)
  problems(dat)
Rows: 8860 Columns: 168
── Column specification ────────────────────────────────────────────────────────
Delimiter: ","
dbl (164): id, WTDRD1, WTDR2D, DR1DRSTZ, DR1EXMER, DRABF, DRDINT, DR1DBIH, D...
lgl   (4): DRQSDT5, DRQSDT6, DRD350JQ, DRD370PQ

ℹ Use `spec()` to retrieve the full column specification for this data.
ℹ Specify the column types or set `show_col_types = FALSE` to quiet this message.
demo <- read_csv("nhanes_demographics.csv")
Rows: 11933 Columns: 27
── Column specification ────────────────────────────────────────────────────────
Delimiter: ","
dbl (27): id, SDDSRVYR, RIDSTATR, gender, age, RIDAGEMN, RIDRETH1, race, RID...

ℹ Use `spec()` to retrieve the full column specification for this data.
ℹ Specify the column types or set `show_col_types = FALSE` to quiet this message.

Data Cleaning

My data wrangling began by using AI to convert my datasets from the XPT format exported by the CDC to the more usable CSV format (Anthropic, 2026). Data cleaning was relatively straightforward. I joined the dataset for participant demographics with the dataset for food intake using the unique ID numbers given to participants. I filtered for only individuals above 18, converted sodium into grams instead of milligrams, removed NA data, and selected the relevant columns. Note that I originally used more than these final 4 predictor variables in my linear model. However, I removed variables that were highly correlated with others or were not significant to produce this cleaner final model. I attempted to improve the diagnostic plots by using log(age) as my response variable, but that did not make a meaningful difference, so I kept just age as my response variable.

# Joining my two datasets by the ID number that identifies respondents. Using inner join to only keep respondents in both datasets.
join_nhanes <- totals %>%
  inner_join(demo, by="id")

only_adult <- join_nhanes %>%
# # Filtering for only adults (older than 18). 
  filter(age>18) %>%
# Mutating sodium to be in grams instead of milligrams because sodium consumption is so high.
  mutate(new_sodium = sodium/1000) %>%
# Filtering out NA values in the columns I plan to use. 
  filter(!is.na(new_sodium), !is.na(caffeine), !is.na(cholesterol), !is.na(fiber)) %>%
# Selecting only the columns I plan to use in my regression. 
  select(id, age, new_sodium, caffeine, cholesterol, fiber)

Generating a Linear Model

# Using the lm function to generate a linear model predicting age based on all the nutrition factors listed above. 
nhanes_lm <- lm(age~new_sodium + caffeine + cholesterol + fiber, only_adult)
summary(nhanes_lm)

Call:
lm(formula = age ~ new_sodium + caffeine + cholesterol + fiber, 
    data = only_adult)

Residuals:
    Min      1Q  Median      3Q     Max 
-46.365 -14.226   3.667  13.420  38.173 

Coefficients:
             Estimate Std. Error t value Pr(>|t|)    
(Intercept) 56.057667   0.582517  96.234  < 2e-16 ***
new_sodium  -2.207230   0.193002 -11.436  < 2e-16 ***
caffeine     0.010786   0.001365   7.899 3.45e-15 ***
cholesterol  0.003375   0.001071   3.152  0.00163 ** 
fiber        0.150511   0.026742   5.628 1.92e-08 ***
---
Signif. codes:  0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1

Residual standard error: 17.06 on 4881 degrees of freedom
Multiple R-squared:  0.03611,   Adjusted R-squared:  0.03532 
F-statistic: 45.71 on 4 and 4881 DF,  p-value: < 2.2e-16
# Plotting linear model diagnostic plots. 
autoplot(nhanes_lm, 1:4, nrow=2, ncol=2)
Warning: `fortify(<lm>)` was deprecated in ggplot2 4.0.0.
ℹ Please use `broom::augment(<lm>)` instead.
ℹ The deprecated feature was likely used in the ggfortify package.
  Please report the issue at <https://github.com/sinhrks/ggfortify/issues>.
Warning: `aes_string()` was deprecated in ggplot2 3.0.0.
ℹ Please use tidy evaluation idioms with `aes()`.
ℹ See also `vignette("ggplot2-in-packages")` for more information.
ℹ The deprecated feature was likely used in the ggfortify package.
  Please report the issue at <https://github.com/sinhrks/ggfortify/issues>.
Warning: Using `size` aesthetic for lines was deprecated in ggplot2 3.4.0.
ℹ Please use `linewidth` instead.
ℹ The deprecated feature was likely used in the ggfortify package.
  Please report the issue at <https://github.com/sinhrks/ggfortify/issues>.

# Generating a correlation matrix to ensure no two predictor variables are highly colinear. 
library(DataExplorer)
Warning: package 'DataExplorer' was built under R version 4.6.1
plot_correlation(only_adult)

The equation generated by my linear model is: age(years) = 56.06 + 0.01*caffeine(mg) + 0.003*cholesterol(mg) + 0.15*fiber(g) - 2.2*sodium(g).

P-values are as follows:

Sodium = 2e-16

Caffeine = 3.45e-15

Cholesterol = 0.00163

Fiber = 1.92e–08

R-squared: 0.03532

Variation in sodium, caffeine, cholesterol, and fiber consumption explains about 3.5% of the variation in age in this dataset. For every 1g increase in sodium consumption, predicted age decreases by 2.2 years. For every 1mg increase in caffeine consumption, predicted age increases by 0.01 years. For every 1mg increase in cholesterol consumption, predicted age increases by 0.003 years. For every 1g increase in fiber consumption, predicted age increases by 0.15 years. While the coefficients on caffeine, cholesterol, and fiber may seem small, individuals may consume tens or hundreds of milligrams/grams of these nutrients per day. Someone looking to decrease their caffeine or cholesterol consumption would not aim to decrease by just one milligram, but perhaps 50 milligrams. In this case, for every 50mg increase in caffeine consumption, predicted age increases by 0.5 years. For every 50mg increase in cholesterol consumption, predicted age increases by 0.15 years. Similarly, someone aiming to increase fiber consumption might aim to increase by 10 grams. For every 10g increase in fiber consumption, predicted age increases by 1.5 years.

Note that the residuals vs fitted plot indicates that the data may not be linear (there is a downward slope in the blue line when linear data would be flat) and the residuals are not evenly distributed (at lower fitted values, the residuals are most often positive, and at higher fitted values, the residuals are most often negative). The q-q plot indicates that the residuals are not normally distributed and extreme values may be skewing the data (the residuals diverge from the expected value for a normal distribution at extreme values). Thus the assumptions of a linear relationship, normality of the residuals, and homoscedasticity of error variance are violated. Because these are 3 of the 5 key assumptions required for a reliable linear model, this data is flawed and the linear model may be inaccurate. The assumption of no collinearity is met. The assumption of independence of observations is also met since there is no time or order aspect to this data.

Creating Final Visualizations

# Using pivot_longer to condense all my dependent variables into one column for ease of plotting.
long_data <- only_adult %>%
  pivot_longer(cols=c(new_sodium, caffeine, cholesterol, fiber), names_to="metric", values_to="measure") %>%
# Using mutate and case_when to change the names inside my metric column so they show up with proper capatalization in plotly.
  mutate(metric=case_when(
    metric=="new_sodium" ~ "Sodium",
    metric=="caffeine" ~ "Caffeine",
    metric=="cholesterol" ~ "Cholesterol",
    metric=="fiber" ~ "Fiber"
                       ))
# Grouping my data by age group and metric and taking the mean of the measures for each group, so I am not plotting thousands of individual datapoints. 
sum_data <- long_data %>%
  group_by(age, metric) %>%
  summarize(n=n(), mean=mean(measure))
`summarise()` has regrouped the output.
ℹ Summaries were computed grouped by age and metric.
ℹ Output is grouped by age.
ℹ Use `summarise(.groups = "drop_last")` to silence this message.
ℹ Use `summarise(.by = c(age, metric))` for per-operation grouping
  (`?dplyr::dplyr_by`) instead.
sum_data
# A tibble: 248 × 4
# Groups:   age [62]
     age metric          n   mean
   <dbl> <chr>       <int>  <dbl>
 1    19 Caffeine       88  37.9 
 2    19 Cholesterol    88 265.  
 3    19 Fiber          88  12.7 
 4    19 Sodium         88   2.90
 5    20 Caffeine       29  69.4 
 6    20 Cholesterol    29 271.  
 7    20 Fiber          29  17.7 
 8    20 Sodium         29   3.61
 9    21 Caffeine       47  66.3 
10    21 Cholesterol    47 352.  
# ℹ 238 more rows
# Using ggplot to create a plot of my summarized data with mean on the x axis, age on the y axis (to follow my lm model), and different color lines/dots for different nutrients.
final_plot <- ggplot(sum_data, aes(x=mean, y=age, color=metric)) +
# Adding lines to my plot.
  geom_line() +
# Adding points to my plot.
  geom_point() +
# Changing the theme.
  theme_minimal() +
# Chanigng the color palette and naming the ledgend. 
  scale_color_brewer(name="Nutrient", palette = "Set2") +
# Using a log scale for x because sodium values are so much smaller than cholesterol. Naming the x axis.
  scale_x_log10("Mean Intake (mg, g for sodium and fiber)") +
# Adding a title, y axis label, and caption.
  labs(title="Mean Intake of Common Nutrients Across Differnt Age Groups", y="Age Group (years)", caption="Data from CDC NHANES 2021-2023")
final_plot 

# Making the final plot interactive using plotly. 
int_final_plot <- ggplotly(final_plot)
int_final_plot

The above visualization shows mean intake of all 4 nutrients used in my model for each age group on the x axis and age group on the y axis all on a single graph for easy comparison. Sodium and fiber are given in grams and caffeine and cholesterol are given in milligrams. Age group is given in years.

# Making a second final plot where the x and y axis are switched to allow for better visualization of the data even though this does not align with the lm.  
final_plot2 <- ggplot(sum_data, aes(x=age, y=mean, color=metric)) +
  geom_line() +
  geom_point() +
  theme_minimal() +
  scale_color_brewer(name="Nutrient", palette = "Set3") +
  scale_y_log10("Mean Intake (mg, g for sodium and fiber)") +
  labs(title="Mean Intake of Common Nutrients Across Differnt Age Groups", x="Age Group (years)", caption="Data from CDC NHANES 2021-2023")
final_plot2

int_final_plot2 <- ggplotly(final_plot2)
int_final_plot2

The above visualization shows mean intake of all 4 nutrients used in my model for each age group on the y axis and age group on the x axis all on a single graph for easy comparison. Note that in the linear model, age is the response variable, but switching the variables allows for a cleaner graph. Sodium and fiber are given in grams and caffeine and cholesterol are given in milligrams. Age group is given in years.

# Grouping by age and summarizing mean nutrient consumption for the data before using pivot longer to make plots of individual nutrients. 
short_sum <- only_adult %>%
  group_by(age) %>%
  summarize(n=n(), mean_sodium=mean(new_sodium), mean_caffeine=mean(caffeine), mean_cholesterol=mean(cholesterol), mean_fiber=(mean(fiber)))
short_sum
# A tibble: 62 × 6
     age     n mean_sodium mean_caffeine mean_cholesterol mean_fiber
   <dbl> <int>       <dbl>         <dbl>            <dbl>      <dbl>
 1    19    88        2.90          37.9             265.       12.7
 2    20    29        3.61          69.4             271.       17.7
 3    21    47        3.44          66.3             352.       15.7
 4    22    40        3.10          69.5             284.       14.2
 5    23    47        3.18          78.9             331.       13.5
 6    24    41        3.63          92.0             304.       15.6
 7    25    41        3.64          81.9             321.       14.3
 8    26    44        3.00         128.              350.       13.3
 9    27    54        3.86         132.              336.       17.4
10    28    52        3.20         130.              270.       17.4
# ℹ 52 more rows
# Making a plot with age on the y axis and just sodium on the x axis. Using geom_smooth to add a trend line (even though my linear model did not only use sodium).
sodium_plot <- ggplot(short_sum, aes(x=mean_sodium, y=age)) +
  geom_point(color="cadetblue") +
  theme_minimal() +
  geom_smooth(method='lm', se=FALSE, color="lightpink") +
  labs(x="Mean Sodium Consumed (g)", y="Age Group (years)", title="Mean Sodium Consumption by Age Group", caption="Data from CDC NHANES 2021-2023") 
sodium_plot
`geom_smooth()` using formula = 'y ~ x'

The above graph shows mean sodium consumption in grams on the x axis and age group in years on the y axis.

# Making a plot with age on the y axis and just caffeine on the x axis. 
caffeine_plot <- ggplot(short_sum, aes(x=mean_caffeine, y=age)) +
  geom_point(color="thistle4") +
  theme_minimal() +
  geom_smooth(method='lm', se=FALSE, color="rosybrown1") +
  labs(x="Mean caffeine Consumed (mg)", y="Age Group (years)", title="Mean Caffeine Consumption by Age Group", caption="Data from CDC NHANES 2021-2023") 
caffeine_plot
`geom_smooth()` using formula = 'y ~ x'

The above graph shows mean caffeine consumption in milligrams on the x axis and age group in years on the y axis.

# Making a plot with age on the y axis and just cholesterol on the x axis. 
cholesterol_plot <- ggplot(short_sum, aes(x=mean_cholesterol, y=age)) +
  geom_point(color="plum3") +
  theme_minimal() +
  geom_smooth(method='lm', se=FALSE, color="salmon") +
  labs(x="Mean Cholesterol Consumed (mg)", y="Age Group (years)", title="Mean Cholesterol Consumption by Age Group", caption="Data from CDC NHANES 2021-2023") 
cholesterol_plot
`geom_smooth()` using formula = 'y ~ x'

The above graph shows mean cholesterol consumption in milligrams on the x axis and age group in years on the y axis.

# Making a plot with age on the y axis and just fiber on the x axis. 
fiber_plot <- ggplot(short_sum, aes(x=mean_fiber, y=age)) +
  geom_point(color="palevioletred1") +
  theme_minimal() +
  geom_smooth(method='lm', se=FALSE, color="paleturquoise3") +
  labs(x="Mean Fiber Consumed (g)", y="Age Group (years)", title="Mean Fiber Consumption by Age Group", caption="Data from CDC NHANES 2021-2023") 
fiber_plot
`geom_smooth()` using formula = 'y ~ x'

The above graph shows mean fiber consumption in grams on the x axis and age group in years on the y axis.

Conclusion

While I did find a significant relationship between age and sodium, caffeine, cholesterol, and fiber consumption, the impact of these variables on age prediction is unimpressive, and they only explain a very small amount of variation in the data. The visualizations help explain why these nutrients are such poor predictors. While there might be an upward trend in caffeine consumption and downward trend in sodium consumption with age, the data swings significantly between age groups with lots of extreme values. Futhermore, the downward and upward trends in cholesterol and fiber respectively are extremely slight. I was surprised that nutrient consumption did not explain more variation in the data and that there are so many outliners in the data. I did not use the weighting techniques the CDC applies when analyzing the data to ensure it matches the demographics of the US, since some populations are over sampled, so it is possible that my results would have been different had I weighted the data according to the CDC’s metrics. Furthermore, the diagnostic plots for the linear model show that the data violates key assumptions required for a reliable linear model, which could explain why a linear model found these variables to have so little impact.

Citations

Anthropic. (2026). Converting nhanes XPT files to CSV[Generative AI Chat]. Claude Sonnet 5 High Thinking. https://claude.ai/share/2eb40506-ab10-41a6-99a7-e457c07138f9

CDC. (2024a). About NHANES. In National Health and Nutrition Examination Survey. National Center for Health Statistics. https://www.cdc.gov/nchs/nhanes/about/index.html

CDC. (2024b). What NHANES covers and how it works. In National Health and Nutrition Examination Survey. National Center for Health Statistics. https://www.cdc.gov/nchs/nhanes/about/survey-content-operations.html

CDC. (2024c). Who participates in NHANES. In National Health and Nutrition Examination Survey. National Center for Health Statistics. https://www.cdc.gov/nchs/nhanes/about/who-participates.html

Williamson, L. (2024). The changing nutritional needs of older adults and how to meet them. In www.heart.org. American Heart Association. https://www.heart.org/en/news/2024/12/18/the-changing-nutritional-needs-of-older-adults-and-how-to-meet-them

Young, N., & Branum, A. (2026). Caffeine consumption among youth ages 2–19: United States, august 2021–2023. CDC. https://www.cdc.gov/nchs/products/databriefs/db566.htm