# Hospitals link
hospitals <- st_read("https://raw.githubusercontent.com/ujhwang/urban-analytics-2025/main/Assignment/mini_3/hospital_11counties.geojson", quiet = TRUE) %>%
mutate(n = 1)
#Hides messages
library(tigris)
# Download county boundaries for Georgia
atlanta_MPO <- tigris::counties(state = "GA", year = 2023, cb = TRUE, progress_bar = FALSE) %>%
dplyr::filter(NAME %in% c("Cherokee","Clayton","Cobb","DeKalb","Douglas",
"Fayette","Forsyth","Fulton","Gwinnett","Henry","Rockdale"))
#Census data in order to normalize and plot by population
census_var <- c(
hhincome = "B19019_001E",
race.total = "B02001_001E",
race.white = "B02001_002E",
race.black = "B02001_003E",
pop_total = "B01003_001E"
)
census <- get_acs(geography = "tract", state = "GA",
county = c("Cherokee","Clayton","Cobb","DeKalb","Douglas",
"Fayette","Forsyth","Fulton","Gwinnett","Henry","Rockdale"),
output = "wide", geometry = TRUE, year = 2023,
variables = census_var)
summarise_mean <- c(names(census_var),
"dist_nearest_mi", "hosp_within_3mi")
#Access metrics
census_5070 <- census %>% st_transform(5070)
hospitals_5070<- hospitals %>% st_transform(5070)
#Tract centroids and distances
centroids <- st_centroid(st_geometry(census_5070))
nearest_ix <- st_nearest_feature(centroids, hospitals_5070)
nearest_dist_m <- st_distance(centroids, hospitals_5070[nearest_ix,], by_element = TRUE)
dist_tbl <- tibble(GEOID = census$GEOID,
dist_nearest_mi = as.numeric(nearest_dist_m) / 1609.34)
#Count hospitals within 3 miles of tract centroid
buf3mi <- st_buffer(centroids, 3 * 1609.34)
h_within <- lengths(st_intersects(buf3mi, hospitals_5070))
within_tbl <- tibble(GEOID = census$GEOID, hosp_within_3mi = h_within)
#Join the access metrics back to census
census <- census %>%
left_join(dist_tbl, by = "GEOID") %>%
left_join(within_tbl, by = "GEOID")
#Summarize pattern
census_hospitals <- census %>%
separate(col = NAME, into=c("tract","county","state"), sep='; ') %>%
#Spatial join
st_join(hospitals %>%
st_transform(crs = st_crs(census))) %>%
#Group_by
group_by(GEOID, county) %>%
#Mean for all census variables + access metrics, sum for n
summarise(
across(all_of(summarise_mean), \(x) mean(x, na.rm = TRUE)),
n = sum(n, na.rm = TRUE),
.groups = "drop"
) %>%
# Replace NA in column n & hosp_within_3mi with 0
mutate(across(c(n, hosp_within_3mi), \(x) ifelse(is.na(x), 0, x)))
# Only runs interactively (skipped during knit)
#Leaflet
#Ran ls() to see which objects there are in this map as my knit keeps stopping here but chunk runs by itself.
library(leaflet)
#1. Pre-transform to WGS84 (HTML maps require 4326)
census_4326 <- sf::st_transform(census, 4326)
hospitals_4326 <- sf::st_transform(hospitals, 4326)
#2. Palette for the legend
fill_pal <- colorQuantile("YlOrRd", domain = census_hospitals$dist_nearest_mi)
#3. Build the map object
m <- leaflet() %>%
setView(lng = -84.3904, lat = 33.7707, zoom = 11) %>%
addProviderTiles(providers$CartoDB.DarkMatterNoLabels) %>%
addPolygons(data = sf::st_union(census_4326),
opacity = 0.2, fillOpacity = 0, weight = 1, color = "white") %>%
addCircleMarkers(data = hospitals_4326,
radius = 3, opacity = 0.8,
fillColor = "red", weight = 1, color = "orange",
popup = ~labels, label = ~labels)
#4. Add legend & title
m <- addLegend(m, position = "bottomright",
pal = fill_pal,
values = census_hospitals$dist_nearest_mi,
title = "Nearest Hospital (mi)",
opacity = 1)
m <- addControl(m, title, position = "topleft", className = "map_title")
m
#Static Maps
ggplot(data = hospitals) +
annotation_map_tile(type = "cartolight") +
geom_point(aes(
x = st_coordinates(hospitals)[,1],
y = st_coordinates(hospitals)[,2]
),
size = 2, alpha = 0.9, color = "purple") +
theme_void() +
labs(title = "Hospital Distribution in Metro Atlanta, GA")

#Scatter Plot
ggplot(data = census_hospitals) +
geom_point(mapping = aes(x = hosp_within_3mi, y = dist_nearest_mi))

#Aesthetic Mapping
ggplot(data = census_hospitals) +
geom_point(mapping = aes(x = hosp_within_3mi, y = dist_nearest_mi,
color = hhincome)) +
scale_color_gradient(low = "darkblue", high = "red")

ggplot(data = census_hospitals) +
geom_point(mapping = aes(x = hosp_within_3mi, y = dist_nearest_mi,
size = pop_total))

#Aesthetic Mapping PT 2 with Nearest Distance
fig1 <- ggplot(data = census_hospitals) +
geom_point(mapping = aes(x = hosp_within_3mi, y = dist_nearest_mi,
alpha = dist_nearest_mi))
fig2 <- ggplot(data = census_hospitals) +
geom_point(mapping = aes(x = hosp_within_3mi, y = dist_nearest_mi,
alpha = dist_nearest_mi,
size = dist_nearest_mi))
gridExtra::grid.arrange(fig1, fig2, ncol= 1)

#Separating plots by categorical values
ggplot(data = census_hospitals) +
geom_point(mapping = aes(x=hosp_within_3mi, y=dist_nearest_mi)) +
facet_wrap(~county)

ggplot(data = census_hospitals) +
geom_point(mapping = aes(x=hosp_within_3mi, y=dist_nearest_mi)) +
facet_grid(county ~ .) # rows by county (try swapping with . ~ county)

#Regression line
ggplot(data = census_hospitals) +
geom_smooth(mapping = aes(x=hosp_within_3mi, y=dist_nearest_mi), method = "lm")

#More than one layers
ggplot(data = census_hospitals) +
geom_point(mapping = aes(x=hosp_within_3mi, y=dist_nearest_mi)) +
geom_smooth(mapping = aes(x=hosp_within_3mi, y=dist_nearest_mi), method = "lm")

#Add a specific mapping to a layer
ggplot(data = census_hospitals, mapping = aes(x=hosp_within_3mi, y=dist_nearest_mi)) +
geom_point() +
geom_smooth(mapping = aes(color = county),
method = "lm")

ggplot(data = census_hospitals,
mapping = aes(x=hosp_within_3mi, y=dist_nearest_mi)) +
geom_point() +
geom_smooth(mapping = aes(color = county), method = "lm") +
labs(
x = "Hospitals within 3 miles",
y = "Distance to Nearest Hospital (mi)",
color = "County in Census",
title = "Do tracts with more nearby hospitals have shorter distances?"
) +
theme_bw() +
theme(
plot.title = element_text(
hjust = 0.5, face = "bold", size = 14, vjust = 3 #This is to make my title slightly higher as before it was overlapping with the legend since the legend is so long! Aesthetic purposes only.
),
legend.position = "right",
legend.box.margin = margin(t = 10, r = 10, b = 10, l = 10)
)

#Data Labeling
outliers <- census_hospitals %>%
arrange(desc(dist_nearest_mi)) %>%
slice(1:4)
ggplot(data = census_hospitals,
aes(x=hosp_within_3mi, y=dist_nearest_mi)) +
geom_point(mapping = aes(color = hhincome)) +
geom_point(data = outliers, size = 3, shape = 1, color = "black") +
ggrepel::geom_label_repel(data = outliers, aes(label = GEOID), max.overlaps = Inf) +
labs(x = "Hospitals within 3 miles",
y = "Distance to Nearest Hospital (mi)",
color = "Annual Household Income",
title = "Access Outliers: Longest Distances") +
scale_color_gradient(low="purple", high="pink") +
theme_light()

#Interactive Visualization
library(plotly)
p <- ggplot(data = census_hospitals %>%
drop_na(hosp_within_3mi, dist_nearest_mi, hhincome)) +
geom_point(aes(x = hosp_within_3mi,
y = dist_nearest_mi,
color = hhincome),
na.rm = TRUE) +
labs(x = "Hospitals within 3 miles",
y = "Distance to Nearest Hospital (mi)",
color = "Annual Household Income",
title = "Nearby Hospitals vs. Distance") +
scale_color_gradient(low = "orange", high = "darkred") +
theme_bw()
plotly::ggplotly(p)
#3D Scatter Plot
plotly::plot_ly(census_hospitals %>% st_drop_geometry(),
x = ~hhincome,
y = ~n,
z = ~dist_nearest_mi,
color = ~hosp_within_3mi,
type = "scatter3d",
mode = "markers",
text = ~paste0("Hospitals within 3mi: ", hosp_within_3mi),
hoverinfo = "text")
#Bar plot and count
ggplot(tibble(count = nrow(hospitals))) +
geom_col(aes(x = "Hospitals", y = count)) +
labs(x = NULL, y = "Count")

#See the counts by county
hospitals %>%
st_join(census %>% st_transform(crs = st_crs(hospitals))) %>%
st_set_geometry(NULL) %>%
group_by(NAME) %>%
tally()
## # A tibble: 94 × 2
## NAME n
## <chr> <int>
## 1 Census Tract 10.01; Fulton County; Georgia 1
## 2 Census Tract 101.15; Fulton County; Georgia 2
## 3 Census Tract 101.33; Fulton County; Georgia 1
## 4 Census Tract 116.29; Fulton County; Georgia 1
## 5 Census Tract 116.31; Fulton County; Georgia 1
## 6 Census Tract 116.40; Fulton County; Georgia 1
## 7 Census Tract 116.47; Fulton County; Georgia 2
## 8 Census Tract 116.49; Fulton County; Georgia 1
## 9 Census Tract 116.60; Fulton County; Georgia 1
## 10 Census Tract 119.01; Fulton County; Georgia 3
## # ℹ 84 more rows
#Histogram, box plot, violin plot
ggplot(census_hospitals) +
geom_histogram(aes(x = dist_nearest_mi), bins = 60)

ggplot(census_hospitals) +
geom_histogram(mapping = aes(x = dist_nearest_mi),
bins = 60)

ggplot(census_hospitals) +
geom_histogram(mapping = aes(x = dist_nearest_mi),
bins = 60,
color="black")

ggplot(census_hospitals) +
geom_histogram(mapping = aes(x = dist_nearest_mi),
bins = 60,
color="black") +
scale_x_continuous(breaks=seq(0,30, by=2))

ggplot(census_hospitals) +
geom_histogram(mapping = aes(x = dist_nearest_mi, fill=county),
bins = 60,
color="black",
position = "identity",
alpha = 0.2) +
scale_x_continuous(breaks=seq(0,30, by=2))

ggplot(census_hospitals) +
geom_histogram(mapping = aes(x = dist_nearest_mi, fill=county),
bins = 60,
color="black",
position = "dodge") +
scale_x_continuous(breaks=seq(0,30, by=2))

#2D Histogram
ggplot(census_hospitals) +
geom_bin2d(mapping = aes(x = hosp_within_3mi, y = dist_nearest_mi))

#Box Plot
bxplot <- ggplot(data = census_hospitals) +
geom_boxplot(aes(x=county, y=dist_nearest_mi),
color="black", fill="white")
plotly::ggplotly(bxplot)
a <- ggplot(data = census_hospitals) +
geom_boxplot(aes(x=county, y=dist_nearest_mi),
fill = "white", color = "black")
b <- ggplot(data = census_hospitals) +
geom_boxplot(aes(x=dist_nearest_mi, y=county),
fill="white", color="black")
gridExtra::grid.arrange(a, b)

#Violin plot
vplot <- ggplot(
data = census_hospitals %>% st_drop_geometry()
) +
geom_violin(
aes(x = county, y = dist_nearest_mi, fill = county),
color = "black", trim = TRUE
) +
#Flip graph to make it more readable
coord_flip() +
#Keep it simple with the legend
guides(fill = "none") +
labs(
x = "County",
y = "Distance to Nearest Hospital (mi)",
title = "Distribution of Nearest-Hospital Distance by County"
) +
theme_bw()
plotly::ggplotly(vplot)
``` r
#Results:
#The results suggest that hospital distribution across Metro Atlanta is uneven. Central counties, especially Fulton and DeKalb, have a denser cluster of hospitals, while outer counties show fewer facilities, leading to longer travel distances. Regression and visualization results indicate that tracts with higher household income tend to have closer hospital access, while areas with higher poverty counts often face greater distances to care. Overall, the findings highlight inequities in healthcare access, supporting the conclusion that the spatial distribution of hospitals in Metro Atlanta is not entirely equitable.