Mapping Obesity and Fast Food Density in the United States: A Geospatial Analysis of Correlation Patterns

Introduction

Obesity has become one of the most pressing public health challenges in the United States. With more than 40% of American adults classified as obese, understanding the environmental and behavioral factors that influence this trend is critical. One such factor is the accessibility of fast food. This project explores the potential geographic relationship between obesity rates and the density of fast-food restaurants across U.S. states. Using public data from the Centers for Disease Control and Prevention (CDC) and a dataset of major fast-food locations, the project visualizes where obesity rates are highest and analyzes whether particular chains are overrepresented in these regions.

The goal is not to claim direct causation but to visualize patterns and contextualize them geographically using interactive maps and basic statistical comparisons to identify potential correlations between food environment and obesity prevalence.

Methodology

This project used geospatial analysis to explore the potential relationship between adult obesity rates and fast-food restaurant density across the United States. Public health data from the CDC’s PLACES dataset were combined with geolocation data for major fast-food chains to visualize and compare obesity patterns at the county and state levels.

The analysis began with a national map illustrating obesity rates across all U.S. counties, which established a broad spatial understanding of where obesity is most prevalent. From there, the focus narrowed to the four states with the highest average obesity rates—Mississippi, Louisiana, Oklahoma, and Alabama—and the four with the lowest, allowing for direct regional comparison.

To measure whether certain fast-food brands were disproportionately represented in more or less obese states, restaurant counts were standardized using z-scores that compared each state’s totals against national averages. This combination of spatial visualization and basic statistical comparison provided a clear framework for exploring how the density of specific fast-food chains may correspond with regional obesity trends.

Importing CDC Obesity Data

First, I loaded the necessary packages for this project.

Show the code
library(tidyverse)
library(leaflet)
library(sf)
library(tigris)
options(tigris_use_cache = TRUE)

Next, I uploaded the CSV file downloaded from the CDC’s PLACES data set and cleaned it to only include data on the column with obesity rates titled “obesity among adults” and geospatial data.

Show the code
#Import CDC data set
cdc_data <- read_csv("/Users/zack/Desktop/Elon Work/Fall 2025/MEA3800/Maps/Final/Places_2.csv")
counties_sf <- counties(cb = TRUE, year = 2020, class = "sf")
counties_sf <- counties_sf |>
  st_transform(3857) |>                 
  st_simplify(dTolerance = 1000, preserveTopology = TRUE) |>  
  st_transform(st_crs(counties_sf)) |>
  st_collection_extract("POLYGON") |>   
  dplyr::filter(!st_is_empty(geometry)) 

#Clean data set to only include obesity rates and geospatial data
obesity <- cdc_data |>
  filter(Measure == "Obesity among adults") |>
  select(LocationName, StateAbbr, Data_Value, LocationID) |>
  rename(
    County = LocationName,
    State = StateAbbr,
    obesity_rate = Data_Value,
    GEOID = LocationID
  )

# join obesity to county geometries
county_obesity <- counties_sf |>
  left_join(obesity, by = "GEOID")

Mapping Obesity

I originally did not have a concrete methodology for this project, I was simply interested in exploring a potential relationship between obesity rates and various fast food chains. To start building a methodology, I created a choropleth map of obesity rates by county for all states in an effort to visualize trends.

Show the code
#Create color palette for choropleth map
pal_obesity <- colorNumeric("Reds", domain = county_obesity$obesity_rate)

#Create choropleth map
leaflet(county_obesity) |>
  addTiles() |>
  addPolygons(
    fillColor = ~pal_obesity(obesity_rate),
    fillOpacity = 0.7,
    color = "white",
    weight = 0.3,
    label = ~paste0(NAME, " County, ", State,
                    " Obesity rate: ", round(obesity_rate, 1), "%"),
    highlight = highlightOptions(weight = 2, color = "#555", bringToFront = TRUE)
  ) |>
  addLegend(
    pal = pal_obesity,
    values = ~obesity_rate,
    title = "Adult Obesity Rate (%)",
    position = "bottomright"
  ) |> 
    setView(lng = -95, lat = 37, zoom = 3)

Importing Fast Food Data

Now that I had a clear idea of regional obesity trends, I imported a data set of all fast food chains in the United States with geospatial data from Kaggle. I cleaned the data to only include the five most popular fast food chains, McDonalds, Burger King, Chik-fil-A, Subway, and Taco Bell.

Show the code
#Import fast food data set
fast_food <- read_csv("/Users/zack/Desktop/Elon Work/Fall 2025/MEA3800/Maps/Final/FastFoodRestaurants.csv")

#Create set of 5 most popular fast food chains
target_brands <- c("McDonald's","Chick-fil-A","Burger King","Subway","Taco Bell")

#Filter data set to only include target brands
fast_food_clean <- fast_food |>
  mutate(name = as.character(name)) |>
  filter(name %in% target_brands)

fast_food_sf <- st_as_sf(
  fast_food_clean,
  coords = c("longitude", "latitude"),
  crs = 4326
) |>
  st_transform(st_crs(counties_sf))

Merging Data Sets and Condensing to Four States With Highest Obesity Rates

I merged fast food geospatial data and county data to create a visualization of fast food chain locations in the four states with the highest obesity rates: Oklahoma, Louisiana, Mississippi, and Alabama.

Show the code
fast_food_with_county <- st_join(fast_food_sf, counties_sf)

fast_food_counts <- fast_food_with_county |>
  st_drop_geometry() |>
  group_by(GEOID, name) |>
  summarise(n_restaurants = n(), .groups = "drop") |>
  tidyr::pivot_wider(
    names_from = name,
    values_from = n_restaurants,
    values_fill = 0
  )

analysis_sf <- county_obesity |>
  left_join(fast_food_counts, by = "GEOID")

#Zoom and filter to top 4 states
state_obesity <- obesity |>
  group_by(State) |>
  summarise(mean_obesity = mean(obesity_rate, na.rm = TRUE)) |>
  arrange(desc(mean_obesity))

head(state_obesity, 5)
# A tibble: 5 × 2
  State mean_obesity
  <chr>        <dbl>
1 MS            43.4
2 LA            42.2
3 OK            42.0
4 AL            41.2
5 WV            40.9
Show the code
target_states <- c("MS", "LA", "OK", "AL")

# counties in those states
southern_sf <- analysis_sf |>
  filter(State %in% target_states)

# get fast-food points that fall in those 4 states
southern_fast_food <- fast_food_sf |>
  st_join(counties_sf |> select(GEOID, STATEFP, STUSPS)) |>
  filter(STUSPS %in% target_states)

# palette for just those 4 states
pal_south <- colorNumeric("Reds", domain = southern_sf$obesity_rate)

#Mapping the states
leaflet(southern_sf) |>
  addTiles() |>
  addPolygons(
    fillColor = ~pal_south(obesity_rate),
    fillOpacity = 0.7,
    color = "white",
    weight = 0.3,
    label = ~paste0(NAME, " County, ", State,
                    " Obesity rate: ", round(obesity_rate, 1), "%"),
    highlight = highlightOptions(weight = 2, color = "#555", bringToFront = TRUE),
    group = "Obesity rate"
  ) |>
  addCircleMarkers(
    data = southern_fast_food |> filter(name == "McDonald's"),
    radius = 2, color = "gold", fillOpacity = 0.7, label = ~name, group = "McDonald's"
  ) |>
  addCircleMarkers(
    data = southern_fast_food |> filter(name == "Chick-fil-A"),
    radius = 2, color = "orange", fillOpacity = 0.7, label = ~name, group = "Chick-fil-A"
  ) |>
  addCircleMarkers(
    data = southern_fast_food |> filter(name == "Burger King"),
    radius = 2, color = "blue", fillOpacity = 0.7, label = ~name, group = "Burger King"
  ) |>
  addCircleMarkers(
    data = southern_fast_food |> filter(name == "Taco Bell"),
    radius = 2, color = "purple", fillOpacity = 0.7, label = ~name, group = "Taco Bell"
  ) |>
  addCircleMarkers(
    data = southern_fast_food |> filter(name == "Subway"),
    radius = 2, color = "green", fillOpacity = 0.7, label = ~name, group = "Subway"
  ) |>
  addLegend(
    pal = pal_south,
    values = ~obesity_rate,
    title = "Adult Obesity Rate (%)",
    position = "bottomright"
  ) |>
  addLayersControl(
    baseGroups = c("Obesity rate"),
    overlayGroups = c("McDonald's","Chick-fil-A","Burger King","Subway","Taco Bell"),
    options = layersControlOptions(collapsed = FALSE),
    position = "topright"
  )

Calculate Representation in Four Target States for Each Target Fast Food Brand

Next, I calculated the average number of locations per target brand among states and found the z-score for each state. I then found the average z-score for each target brand among my four target states.

Show the code
# 1. Get state geometries
states_sf <- states(cb = TRUE, year = 2020, class = "sf") |>
  select(STUSPS)

# 2. Join restaurants to states and count per chain per state
fast_food_by_state <- fast_food_sf |>
  st_join(states_sf) |>
  st_drop_geometry() |>
  group_by(STUSPS, name) |>
  summarise(n_restaurants = n(), .groups = "drop")

# 3. Compute national averages and standard deviations for each chain
brand_stats <- fast_food_by_state |>
  group_by(name) |>
  summarise(
    mean_count = mean(n_restaurants, na.rm = TRUE),
    sd_count   = sd(n_restaurants, na.rm = TRUE),
    .groups = "drop"
  )

# 4. Add z-scores to each state/chain
fast_food_z <- fast_food_by_state |>
  left_join(brand_stats, by = "name") |>
  mutate(z_score = (n_restaurants - mean_count) / sd_count)

# 5. Filter to the 4 most obese states
top_obese_states <- c("MS", "LA", "OK", "AL")
south_z <- fast_food_z |> filter(STUSPS %in% top_obese_states)

# 6. View per-state z-scores
south_z |>
  arrange(STUSPS, desc(z_score)) |>
  print(n = 20)
# A tibble: 19 × 6
   STUSPS name        n_restaurants mean_count sd_count z_score
   <chr>  <chr>               <int>      <dbl>    <dbl>   <dbl>
 1 AL     Chick-fil-A            10       3.12     2.59  2.66  
 2 AL     Taco Bell              29      17.8     16.7   0.670 
 3 AL     Burger King            25      23.6     20.2   0.0716
 4 AL     McDonald's             28      36.3     31.5  -0.262 
 5 AL     Subway                  2       6.44     5.29 -0.840 
 6 LA     McDonald's             55      36.3     31.5   0.594 
 7 LA     Taco Bell              24      17.8     16.7   0.370 
 8 LA     Burger King            26      23.6     20.2   0.121 
 9 LA     Subway                  7       6.44     5.29  0.106 
10 LA     Chick-fil-A             2       3.12     2.59 -0.435 
11 MS     Taco Bell              11      17.8     16.7  -0.408 
12 MS     McDonald's             16      36.3     31.5  -0.643 
13 MS     Burger King            10      23.6     20.2  -0.670 
14 MS     Subway                  2       6.44     5.29 -0.840 
15 OK     McDonald's             50      36.3     31.5   0.435 
16 OK     Taco Bell              20      17.8     16.7   0.131 
17 OK     Subway                  6       6.44     5.29 -0.0832
18 OK     Chick-fil-A             2       3.12     2.59 -0.435 
19 OK     Burger King             6      23.6     20.2  -0.867 
Show the code
# 7. Average Z-score per chain
south_chain_summary <- south_z |>
  group_by(name) |>
  summarise(
    avg_z_score = mean(z_score, na.rm = TRUE),
    .groups = "drop"
  ) |>
  arrange(desc(avg_z_score))

south_chain_summary
# A tibble: 5 × 2
  name        avg_z_score
  <chr>             <dbl>
1 Chick-fil-A      0.596 
2 Taco Bell        0.191 
3 McDonald's       0.0311
4 Burger King     -0.336 
5 Subway          -0.414 

Calculate Representation in Four States With Lowest Obesity Rate for Each Target Fast Food Brand

I then did the same for the four states with the lowest obesity rate: Washington DC, Hawaii, Colorado, and Vermont. Unfortunately, the data set I had did not have any Chik-fil-A chains listed, so I was only able to calculate an average z-score for these four states for Subway, Burger King, Taco Bell, and McDonald’s.

Show the code
#Rebuild state obesity table from raw CDC data
state_obesity_full <- cdc_data |>
  filter(Measure == "Obesity among adults") |>
  group_by(StateAbbr) |>
  summarise(
    mean_obesity = mean(Data_Value, na.rm = TRUE),
    .groups = "drop"
  ) |>
  filter(nchar(StateAbbr) == 2) |>
  arrange(mean_obesity)

# 4 least obese states
least_states <- state_obesity_full |>
  head(4) |>
  pull(StateAbbr)

least_states
[1] "DC" "HI" "CO" "VT"
Show the code
# tag fast food with state via spatial join
fast_food_with_state <- fast_food_sf |>
  st_join(counties_sf |> select(GEOID, STUSPS))

# keep only the least-obese states
least_fast_food <- fast_food_with_state |>
  filter(STUSPS %in% least_states)

# count per chain per (least) state
least_ff_by_state <- least_fast_food |>
  st_drop_geometry() |>
  group_by(STUSPS, name) |>
  summarise(n_restaurants = n(), .groups = "drop")

# reuse brand_stats from before (national mean/sd per chain)
least_z <- least_ff_by_state |>
  left_join(brand_stats, by = "name") |>
  mutate(z_score = (n_restaurants - mean_count) / sd_count)

# average z per chain across the least-obese states
least_chain_summary <- least_z |>
  group_by(name) |>
  summarise(
    avg_z_score = mean(z_score, na.rm = TRUE),
    .groups = "drop"
  ) |>
  arrange(desc(avg_z_score))

least_chain_summary
# A tibble: 4 × 2
  name        avg_z_score
  <chr>             <dbl>
1 Subway           -0.225
2 Burger King      -0.620
3 Taco Bell        -0.723
4 McDonald's       -0.833

Findings

The four most obese states - Mississippi, Louisiana, Oklahoma, and Alabama - showed a clear regional clustering of higher obesity rates, particularly in the Deep South. The choropleth map revealed extensive dark red areas, signifying obesity rates above 40% in many counties. When overlaid with fast-food restaurant data, these same regions also displayed high concentrations of major chains, especially along major highways and urban centers.

Statistical analysis using z-scores quantified each brand’s relative presence compared to its national average. Among the most obese states, Chick-fil-A had the highest average z-score (+0.596), indicating a moderate overrepresentation relative to the national baseline. Taco Bell (+0.191) and McDonald’s (+0.031) were close to their expected averages, while Burger King (-0.336) and Subway (-0.414) appeared slightly underrepresented.

The four least obese states - Colorado, Hawaii, Vermont, and DC - showed negative z-scores across all chains, suggesting below-average fast-food density. Subway was the least negative (-0.225), while McDonald’s showed the largest deviation (-0.833). This indicates that, collectively, fast-food restaurant counts in low-obesity states fall below the national mean, aligning with the sparser distribution visible on the corresponding map.

Conclusion

This analysis found that the most obese states have both higher average obesity rates and slightly above-average fast-food density, while the least obese states show the opposite trend. Though the magnitude of these differences is not large, the directionality across all five major chains is consistent, suggesting that access to fast food may contribute to, but does not solely determine, regional obesity patterns.

The project demonstrates how publicly available health and business location data can be integrated to reveal meaningful spatial relationships. Future research could extend this analysis by normalizing restaurant counts to population size, exploring income and urbanization as moderating variables, or using time-series data to track how fast-food expansion correlates with long-term obesity changes.

By combining mapping, descriptive statistics, and comparative visualization, this study provides an accessible yet data-driven look at how geography and food environments intersect with public health outcomes in the United States.