The 2016 Penny Tax initiative in Palm Beach County sought to provide essential funding for school repairs, renovations, and improvements through a 1% increase on local sales tax. Approved by 56.3% of voters during the November 2016 General Election, the tax was labeled on the ballot as County Question 1. This project examines the potential relationship between economic status and voting behavior. By integrating precinct-level election results with economic markers, like median household income from the Census Bureau at the block group level, this analysis seeks to explore the economic influence on voter approval of the 2016 Penny Tax. The graphs below seek to reveal patterns in voter support for school funding initiatives linked to sales tax increases, as well as provide insights into community characteristics of Palm Beach County precincts.
library(tidyverse)
library(readxl)
library(sf)
library(tidycensus)
library(ggplot2)
library(dplyr)
library(patchwork)
The 2016 November General Election results can be found through the Florida Division of Elections site. These files were downloaded as an Excel spreadsheet to clean and sort through the data, and moved into a CSV file with the condensed information.
election_data2016 <- read_csv("/Users/dalia/Downloads/election_data2016(Sheet1).csv")
election_data2016 <- election_data2016 |>
mutate(
`For Tax` = if_else(is.na(`For Tax`), 0, `For Tax`),
`Against Tax` = if_else(is.na(`Against Tax`), 0, `Against Tax`)
)
I was able to obtain the shapefiles necessary to visualize the Palm Beach Precincts through the UF Election Data Lab website.
precinct_map <- st_read("/Users/dalia/Downloads/fl_2016")
## Reading layer `fl_2016' from data source `/Users/dalia/Downloads/fl_2016' using driver `ESRI Shapefile'
## Simple feature collection with 6116 features and 18 fields
## Geometry type: MULTIPOLYGON
## Dimension: XY, XYZ
## Bounding box: xmin: -87.6349 ymin: 24.39631 xmax: -79.97431 ymax: 31.00097
## z_range: zmin: 0 zmax: 0
## Geodetic CRS: NAD83
palm_beach_shapefile <- precinct_map |>
filter(COUNTY == "PAL")
plot(st_geometry(palm_beach_shapefile))
This code helped ensure that the precinct numbers were treated as characters and not numerical values.
palm_beach_shapefile$PRECINCT <- as.character(palm_beach_shapefile$PRECINCT)
election_data2016$Precinct <- as.character(election_data2016$Precinct)
This merged the election results CSV file with the Palm Beach shapefile.
palm_beach_shapefile_merged <- palm_beach_shapefile |>
left_join(election_data2016, by = c("PRECINCT" = "Precinct")) |>
mutate(
`For Tax` = if_else(is.na(`For Tax`), 0, `For Tax`),
`Against Tax` = if_else(is.na(`Against Tax`), 0, `Against Tax`)
)
In order to make sure that the merge had been successful, and that each Precinct had been present, I checked the number of rows in the the original shapefile and the newly merged shapefile. This was successful as both numbers matched.
nrow(palm_beach_shapefile)
## [1] 938
nrow(palm_beach_shapefile_merged)
## [1] 938
In order to work with the merged file, the data had to be made valid to work with. Once this was done, I could now make a visual that identified the number of votes present in each precinct in favor of the tax.
palm_beach_shapefile_merged <- st_make_valid(palm_beach_shapefile_merged)
ggplot(data = palm_beach_shapefile_merged) +
geom_sf(aes(fill = `For Tax`)) +
scale_fill_viridis_c() +
labs(title = "Palm Beach County Precincts: Votes For Tax", fill = "Votes For Tax") +
theme_minimal()
Mapping Precincts with Voting Results- Proportion For the Tax:
This map visualizes the proportion, rather than the count, of votes in favor of the tax, per precinct.
palm_beach_shapefile_merged <- palm_beach_shapefile_merged |>
mutate(for_tax_pct = `For Tax` / (`For Tax` + `Against Tax`))
ggplot(data = palm_beach_shapefile_merged) +
geom_sf(aes(fill = for_tax_pct)) +
scale_fill_viridis_c() +
labs(
title = "Support for 2016 Penny Tax by Precinct",
fill = "Proportion For Tax"
) +
theme_minimal()
The median income data was obtained through the TidyCensus package. This information was turned into a data set to work with.
census_data <- get_acs(
geography = "block group",
variables = c(median_income = "B19013_001"),
state = "FL",
county = "Palm Beach",
geometry = TRUE
) |>
select(GEOID, NAME, estimate, geometry) |>
rename(median_income = estimate)
## | | | 0% | |= | 1% | |= | 2% | |== | 2% | |== | 3% | |=== | 4% | |=== | 5% | |==== | 5% | |==== | 6% | |===== | 7% | |===== | 8% | |====== | 8% | |====== | 9% | |======= | 10% | |======= | 11% | |======== | 12% | |========= | 13% | |========== | 14% | |========== | 15% | |=========== | 16% | |============ | 18% | |============== | 20% | |=============== | 21% | |=============== | 22% | |================ | 23% | |================ | 24% | |================= | 25% | |================== | 26% | |=================== | 27% | |=================== | 28% | |==================== | 28% | |==================== | 29% | |===================== | 30% | |====================== | 32% | |======================= | 32% | |======================= | 33% | |======================== | 34% | |========================= | 36% | |========================== | 37% | |=========================== | 39% | |============================ | 40% | |============================ | 41% | |============================= | 42% | |============================== | 43% | |=============================== | 44% | |================================ | 46% | |================================= | 47% | |================================= | 48% | |================================== | 48% | |=================================== | 50% | |===================================== | 54% | |====================================== | 54% | |======================================= | 56% | |======================================== | 57% | |======================================== | 58% | |========================================= | 58% | |========================================= | 59% | |========================================== | 60% | |=========================================== | 61% | |============================================ | 62% | |============================================ | 63% | |============================================== | 65% | |============================================== | 66% | |=============================================== | 66% | |=============================================== | 68% | |================================================ | 69% | |================================================= | 71% | |================================================== | 71% | |================================================== | 72% | |=================================================== | 73% | |==================================================== | 74% | |===================================================== | 75% | |===================================================== | 76% | |====================================================== | 78% | |======================================================= | 78% | |======================================================== | 79% | |======================================================== | 80% | |========================================================= | 81% | |========================================================= | 82% | |========================================================== | 82% | |========================================================== | 84% | |=========================================================== | 84% | |=========================================================== | 85% | |============================================================= | 87% | |=============================================================== | 90% | |================================================================ | 92% | |================================================================== | 94% | |=================================================================== | 95% | |=================================================================== | 96% | |==================================================================== | 96% | |==================================================================== | 97% | |===================================================================== | 98% | |======================================================================| 100%
Necessary to properly merge with the Palm Beach precinct shapefile.
census_data <- st_transform(census_data, st_crs(palm_beach_shapefile_merged))
palm_beach_with_census <- st_join(palm_beach_shapefile_merged, census_data, join = st_intersects)
To make the data clearer, I aggregated the data for precincts that held multiple blocks within them. This would allow the Census data to be reflect all of the blocks within a singular precinct.
PB_with_census_summary <- palm_beach_with_census |>
group_by(PRECINCT) |>
summarize(
avg_median_income = mean(median_income, na.rm = TRUE)
)
With the information now in a singular data set, this visual shows the average median income for the Palm Beach Precincts, rather than for the Census Blocks.
ggplot(data = PB_with_census_summary) +
geom_sf(aes(fill = avg_median_income)) +
scale_fill_viridis_c(na.value = "grey80") +
labs(
title = "Palm Beach Precincts: Average Median Income",
fill = "Median Income"
) +
theme_minimal()
census_block_groups <- get_acs(
geography = "block group",
variables = c(median_income = "B19013_001"),
state = "FL",
county = "Palm Beach",
geometry = TRUE
)
census_block_groups_summary <- census_block_groups |>
mutate(
median_income = estimate
)
This map, unlike the one above, showcases the median average income only for the divisions provided by the Census Block groups. This map, therefore, does not reflect any information about the geographical divisions for electoral precincts.
ggplot(data = census_block_groups_summary) +
geom_sf(aes(fill = median_income)) +
scale_fill_viridis_c(na.value = "grey80") +
labs(
title = "Palm Beach Block Groups: Average Median Income",
fill = "Median Income"
) +
theme_minimal()
PB_with_census_summary_df <- as.data.frame(PB_with_census_summary)
merged_data <- palm_beach_with_census |>
left_join(PB_with_census_summary_df, by = "PRECINCT")
Through this map, we can visualize how as median income increases, there is a slight decrease in the support towards the tax increase. The data points at 1.00 proportion of votes indicate precincts where there were no votes against the tax, whereas points at 0.00 proportion indicate precincts where there were no votes cast in favor of the tax.
ggplot(data = merged_data) +
geom_point(aes(x = avg_median_income, y = `For Tax` / (`For Tax` + `Against Tax`)),
color = "blue", alpha = 0.6) +
geom_smooth(aes(x = avg_median_income, y = `For Tax` / (`For Tax` + `Against Tax`)),
method = "lm", color = "red", se = FALSE) +
labs(
title = "Relationship Between Median Income and Support for Tax",
x = "Average Median Income",
y = "Proportion of Votes For Tax"
) +
theme_minimal()
## `geom_smooth()` using formula = 'y ~ x'
To further visualize similarities and differences between how precincts with higher vs lower median incomes voted for the tax, this visual shows 2 maps next to each other. The first holds information on the median income by precinct, which can be visually compared with the second, which shows which areas voted For the Tax.
p1 <- ggplot(data = merged_data) +
geom_sf(aes(fill = avg_median_income)) +
scale_fill_viridis_c(option = "plasma") +
labs(title = "Median Income by Precinct", fill = "Median Income") +
theme_minimal()
p2 <- ggplot(data = merged_data) +
geom_sf(aes(fill = `For Tax`)) +
scale_fill_viridis_c(option = "plasma") +
labs(title = "Votes For Tax by Precinct", fill = "Votes For Tax") +
theme_minimal()
library(patchwork)
p1 + p2
Through this project, I was able to explore at greater depth the economic status, through median income, of supporters of the 2016 Penny Tax initiative. The regression line analysis was one of the most significant visuals I created to display the possible relationship between these variables, and although slight, there does appear to be a decrease in support for the Penny Tax as the annual median income increases.
The maps within the Graphing Relationships section help visualize the overall support for the Penny tax in Palm Beach, and the Bringing Census Data and Precinct Data Together visuals highlight the work I did to be able to merge Census Block group data with the precinct results. My final map, within the Dual Heatmap to Visualize Votes… section serves as a final visual reference to compare the precincts with the greatest support for the Penny tax and to those with the greatest media incomes in the county.
Overall, some of the biggest challenges I faced throughout this project involved working with the original spreadsheet containing the election results at the precinct level and managing the data sets through the different merges. The documentation from the Florida Division of Elections contained much more information than the necessary for this project. In order to make the data possible to work with, I had to manually sort through precincts to obtain only the Total Count for each. I was able to use R to then adjust the sets when making certain variables characters, managing NAs, etc. In addition, the data sets changed multiple times when working through merges. I was surprised that to make different visuals, I would have to alter my sets in different manners (not a one-size-fits-all scenario like I was hoping for). Finally, I was worried through the merging of the geographies, and had trouble deciding what how to best visually represent the information.
In the end, this project was a great opportunity to work with election data relevant to both my hometown and my passion for school finance research. I believe this project would be helpful in the future when determining the success of similar tax initiatives and tax increases, and could hopefully highlight what populations are more likely to support them. Further research could be conducted to look at other income signifiers, like reliance on SNAP programs, or considerations like voting roll-off.