Show the code
library(tidyverse)
library(leaflet)
library(sf)
library(tigris)
options(tigris_use_cache = TRUE)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.
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.
First, I loaded the necessary packages for this project.
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.
#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")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.
#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)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.
#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))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.
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
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"
)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.
# 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
# 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
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.
#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"
# 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
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.
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.