In this map, I chose Palm Beach County for my analysis. This county is significant to me because this is where I grew up! I analyzed Median Household Income as the ACS variable. In comparison to the neighboring counties, all three counties contain both higher-income and lower-income areas, but the income levels are not evenly distributed. Martin County appears to have more higher-income areas, while Broward County has more middle-income areas. Palm Beach County shows the widest variation in income, with both lower- and higher-income tracts throughout the county. One thing that surprised me was how many different colored areas appeared on the map for Palm Beach County and Broward County. Before creating the map, I expected the counties within to be more similar and in larger groups. Instead, there were large differences in median household income between neighboring Census tracts.
For the Sub-Country Map, the county subdivisions appeared relatively similar, but Martin County does have less variety compared to Palm Beach County and Broward County. Palm Beach County had the highest values, coming in at $100k to $120k in numerous areas. Broward County falls between $60k to $100k, while Martin County had the lowest values on the map. I used Census tracts as the geographic unit of analysis. Based on what I know about Palm Beach County, the high median household income may be related to the rapid growth and development in the area. New apartments, neighborhoods and high-rise buildings are constantly being built, which attracts more people and increases property values. These factors may help explain why income levels are relatively high in the county.
Before beginning the analysis, I expected to find a significant value for the median household income for Palm Beach County but I was interested in seeing how it compared to the counties surrounding it. The results definitely aligned with my expectations. The tract-level map showed differences in income within the counties that were hidden in the county-level map. While the county map showed only an overall income level for each county, the tract map revealed that some areas had much higher incomes and others had much lower incomes. This showed that income is not evenly distributed throughout the counties. If I were developing an academic study, I would focus on what factors influence median household income in different areas of Palm Beach, Martin, and Broward counties. The study could examine whether factors such as housing development, employment opportunities, and population growth are related to higher household incomes.
############################################################
# RUN ONCE TO SAVE YOUR CENSUS API KEY
#
# Run this section once, then delete it, so that the
# script begins with STEP 1, below
############################################################
# if (!require("tidycensus")) install.packages("tidycensus")
# library(tidycensus)
#
# census_api_key(
# "PASTE_YOUR_CENSUS_API_KEY_BETWEEN_THESE_QUOTES",
# install = TRUE,
# overwrite = TRUE
# )
#
# readRenviron("~/.Renviron")
############################################################
# STEP 1: Load Packages
############################################################
if (!require("tidycensus")) install.packages("tidycensus")
if (!require("tidyverse")) install.packages("tidyverse")
if (!require("kableExtra")) install.packages("kableExtra")
if (!require("sf")) install.packages("sf")
if (!require("leaflet")) install.packages("leaflet")
if (!require("htmlwidgets")) install.packages("htmlwidgets")
if (!require("plotly")) install.packages("plotly")
library(tidycensus)
library(tidyverse)
library(kableExtra)
library(sf)
library(leaflet)
library(htmlwidgets)
library(plotly)
############################################################
# STEP 2: Verify Census API Key
############################################################
if (!nzchar(Sys.getenv("CENSUS_API_KEY"))) {
stop(
paste(
"\nNo Census API key was found.",
"\n\nTo obtain a free Census API key, visit:",
"\nhttps://api.census.gov/data/key_signup.html",
"\n\nThen run the one-time setup code at the",
"\ntop of this script to store your key.",
"\n"
)
)
}
############################################################
# STEP 3: Explore 2024 Codebooks and Choose an ACS Variable
############################################################
ProfileTables <- load_variables(2024, "acs5/profile")
# DetailedTables <- load_variables(2024, "acs5")
# SubjectTables <- load_variables(2024, "acs5/subject")
# Replace the variable code below with the ACS variable
# you want to download.
my_variable <- "DP03_0062"
# Example variables:
#
# DP05_0001 = Total population
# DP03_0062 = Median Household Income
# DP03_0128P = Poverty Rate
# DP02_0068P = Bachelor's Degree or Higher Rate
# DP05_0018 = Median Age
# DP04_0046P = Homeownership Rate
# DP04_0134 = Median Gross Rent
# DP04_0142P = Pct. renters paying 35%+ HH income in rent (GRAPI)
# DP05_0090P = Hispanic or Latino Population Percentage
# DP02_0094P = Foreign-Born Percentage
# DP03_0009P = Unemployment Rate
# DP03_0025 = Average Commute Time
############################################################
# STEP 4: Download ACS Data
############################################################
# Common ACS geography options:
#
# "state"
# "county"
# "county subdivision"
# "tract"
# "block group"
# "zcta"
#
# Most of this script works for any ACS geography.
# Some geographies may require additional filtering
# or display adjustments.
mydata <- get_acs(
geography = "tract",
state = "FL",
year = 2024,
survey = "acs5",
variables = my_variable,
geometry = TRUE
)
############################################################
# STEP 5: Keep Needed Variables and Geometry
############################################################
mydata <- mydata |>
select(
Area = NAME,
Variable = variable,
Estimate = estimate,
Margin_of_Error = moe,
geometry
)
############################################################
# STEP 5B: Transform Coordinates for Leaflet
############################################################
mydata <- st_transform(
mydata,
crs = 4326
)
############################################################
# STEP 6: Optional Filtering
############################################################
# OPTION A: Filter for specific counties.
#
# Uncomment to keep only county subdivisions whose
# names contain the specified county names. To search
# for just a single county, use just the county name
# without the | character.
############################################################
# STEP 6: Filter to Selected Counties
############################################################
mydata <- mydata |>
filter(
str_detect(
Area,
"Palm Beach|Martin|Broward"
)
)
############################################################
# STEP 7: Sort Results from Highest to Lowest
############################################################
mydata <- mydata |>
arrange(desc(Estimate))
############################################################
# STEP 8: Display Results as a Table
############################################################
ResultsTable <- mydata |>
st_drop_geometry() |>
kbl(
col.names = c(
"Area",
"Variable",
"Estimate",
"Margin of Error"
),
format.args = list(big.mark = ",")
) |>
kable_styling(
bootstrap_options = c("striped", "hover"),
full_width = FALSE
)
ResultsTable
############################################################
# STEP 9: Create Pop-Up Content
############################################################
mydata$popup <- paste0(
"", mydata$Area, "
",
"Estimate: ",
format(mydata$Estimate, big.mark = ","),
"
",
"Plus/Minus: ",
format(mydata$Margin_of_Error, big.mark = ",")
)
############################################################
# STEP 10: Build Color Palette
############################################################
qs <- unique(
quantile(
mydata$Estimate,
probs = seq(0, 1, length.out = 6),
na.rm = TRUE
)
)
pal <- colorBin(
palette = "Blues",
domain = mydata$Estimate,
bins = qs,
pretty = FALSE
)
############################################################
# STEP 11: Create Choropleth Map
############################################################
AreaMap <- leaflet(mydata) |>
addProviderTiles(
providers$Esri.WorldTopoMap
) |>
addPolygons(
fillColor = ~pal(Estimate),
fillOpacity = 0.5,
color = "black",
weight = 1,
popup = ~popup
) |>
addLegend(
pal = pal,
values = ~Estimate,
title = "Estimate",
labFormat = labelFormat(
big.mark = ","
)
)
AreaMap
############################################################
# STEP 12: Create Plotly Dot Plot
############################################################
plotdf <- mydata |>
st_drop_geometry() |>
mutate(
point_color = pal(Estimate),
y_ordered = reorder(
Area,
Estimate
),
hover_text = paste0(
Area,
"
Estimate: ",
format(Estimate, big.mark = ","),
"
Plus/Minus: ",
format(Margin_of_Error, big.mark = ",")
)
)
DotPlot <- plot_ly(
data = plotdf,
x = ~Estimate,
y = ~as.character(y_ordered),
type = "scatter",
mode = "markers",
showlegend = FALSE,
marker = list(
color = ~point_color,
size = 8,
line = list(
color = "rgba(120,120,120,0.9)",
width = 0.5
)
),
error_x = list(
type = "data",
array = ~Margin_of_Error,
arrayminus = ~Margin_of_Error,
color = "rgba(0,0,0,0.65)",
thickness = 1
),
text = ~hover_text,
hovertemplate = "%{text}"
) |>
layout(
title = list(
text = paste0(
"ACS Estimates by State",
"
Error bars show ACS margins of error."
)
),
xaxis = list(
title = "Estimate",
tickformat = ",.0f",
automargin = TRUE
),
yaxis = list(
title = "",
automargin = TRUE,
categoryorder = "array",
categoryarray = levels(plotdf$y_ordered)
),
margin = list(
l = 200,
r = 20,
b = 60,
t = 60,
pad = 2
)
)
DotPlot
############################################################
# STEP 13: Export Dot Plot (Optional)
############################################################
saveWidget(
widget = as_widget(DotPlot),
file = "ACSGraph.html",
selfcontained = TRUE
)
############################################################
# STEP 14: Export Map (Optional)
############################################################
saveWidget(
widget = AreaMap,
file = "ACSMap.html",
selfcontained = TRUE
)
############################################################
# STEP 15: Download Palm Beach County Subdivision Data
############################################################
SubdivisionData <- get_acs(
geography = "county subdivision",
state = "FL",
year = 2024,
survey = "acs5",
variables = my_variable,
geometry = TRUE
)
############################################################
# STEP 16: Keep Palm Beach County Subdivisions
############################################################
SubdivisionData <- SubdivisionData |>
filter(
str_detect(NAME, "Palm Beach|Martin|Broward")
) |>
select(
Area = NAME,
Variable = variable,
Estimate = estimate,
Margin_of_Error = moe,
geometry
)
SubdivisionData <- st_transform(
SubdivisionData,
crs = 4326
)
############################################################
# STEP 17: Create Popups
############################################################
SubdivisionData$popup <- paste0(
"<b>", SubdivisionData$Area, "</b>",
"<br>Estimate: ",
format(SubdivisionData$Estimate, big.mark = ","),
"<br>Plus/Minus: ",
format(SubdivisionData$Margin_of_Error, big.mark = ",")
)
############################################################
# STEP 18: Create Color Palette
############################################################
SubPal <- colorBin(
palette = "Blues",
domain = SubdivisionData$Estimate,
bins = 5
)
############################################################
# STEP 19: Create Subdivision Map
############################################################
SubdivisionMap <- leaflet(SubdivisionData) |>
addProviderTiles(
providers$Esri.WorldTopoMap
) |>
addPolygons(
fillColor = ~SubPal(Estimate),
fillOpacity = 0.5,
color = "black",
weight = 1,
popup = ~popup
) |>
addLegend(
pal = SubPal,
values = ~Estimate,
title = "Estimate"
)
SubdivisionMap