show the code
library(tidyverse)
library(sf)
library(mapview)
library(vroom)
library(qs)
library(janitor)
library(knitr)
sf_use_s2(FALSE)
The purpose of this exercise is to demonstrate the value of the National Water Service Area Boundary layer with a Federal government use case. This use case is to quantify the extent to which adding a drinking water quality-based indicator to the Climate and Environmental Justice Screening Tool would change the universe of “communities” (2010 Census Tract geographies) that are highlighted. This document describes the how to construct a binary indicator that corresponds to some drinking-water related risk, implements the indicator
The most common way to do this is to calculate an indicator variable based on drinking water violations data from the U.S. EPA SDWIS database. Here, I review and go over the pros and cons of the most common approaches in governance and the literature. First, some necessary terminology:
A violation of regulations of the Safe Drinking Water Act
When a concentration of a contaminant is detected to be above the limit allowed in drinking water standards.
A group of maximum contaminant levels and monitoring requirements for the presence of total coliform, fecal coliform, and E. coli bacteria.
When a concentration of a disinfectant residual is detected to be above the limit allowed in drinking water standards.
When specified treatment techniques required for a system’s water source are not applied.
Violations that are directly related to health risks in drinking water. Corresponds to all MCL, MRDL, and TT violations.
Failure to conduct drinking water quality tests or to submit the results of those tests in a timely fashion to USEPA or the primacy agency.
A USEPA catch-all category for other violations, which are generally either about conducting sanitary surveys of the system or about reporting their test results to the public or water systems they are interconnected with.
Being in a state of not violating a relevant regulation
A period of time when a water system was in violation of a drinking water regulation
There are a few considerations in constructing a binary violation indicator:
A brief review of the literature follows.
Description of metric | Pros | Cons |
---|---|---|
EPA “Enforcement Priority” (formerly “Serious Violator”), a threshold composed of points weighted highest towards TCR violations, less so for other health-based violations like Nitrates andl LCR, and least for repeated reporting violations over a rolling 5-year period. See here. Does not count violations that are returned to compliance or are undergoing a verified enforcement action. |
|
|
All violations, over a multi-year period (Wallsten and Kosec 2008; Konisky and Teodoro 2015; Marcillo and Krometis 2019) |
|
|
TCR MCL violations only, on an annual basis (Allaire, Wu, and Lall 2018) |
|
|
All MCL violations over a multi-year period (Dobbin and Fencl 2021; Switzer and Teodoro 2017) |
|
|
All Health-Based Violations over a multi-year period (Dobbin and Fencl 2021; Switzer and Teodoro 2017) |
|
|
Binary indicator, which is if the system experienced more than 1 month in health-based violation in the past two years. Discussion can modify the month and year periods. I believe giving a grace period for 1 month over two years leaves room for true fluke events for utilities that otherwise have good process control. I can also simply create the metric for a few combinations and we can see what seems reasonable.
First, we load required packages
library(tidyverse)
library(sf)
library(mapview)
library(vroom)
library(qs)
library(janitor)
library(knitr)
sf_use_s2(FALSE)
Below, we describe and implement the workflow in R.
First, we retrieve the .zip archive of the SDWIS data download from USEPA, and unzip the violations data table.
download.file(url = "https://echo.epa.gov/files/echodownloads/SDWA_latest_downloads.zip", destfile = "../data/sdwa.zip")
unzip("data/sdwa.zip",
files = "SDWA_VIOLATIONS_ENFORCEMENT.csv",
exdir = "data")
This archive has several tables, the relevant one being SDWA_VIOLATIONS_ENFORCEMENT.csv
, which as of this version of the workflow, was last updated on April 13, 2022.
unzip("data/sdwa.zip", list = TRUE)
Name Length Date
1 SDWA_EVENTS_MILESTONES.csv 36426509 2022-04-13 15:11:00
2 SDWA_FACILITIES.csv 156229271 2022-04-13 15:13:00
3 SDWA_GEOGRAPHIC_AREAS.csv 40040738 2022-04-13 15:13:00
4 SDWA_LCR_SAMPLES.csv 104175670 2022-04-13 15:13:00
5 SDWA_PUB_WATER_SYSTEMS.csv 126596558 2022-04-13 15:14:00
6 SDWA_PN_VIOLATION_ASSOC.csv 31721515 2022-04-13 15:14:00
7 SDWA_REF_ANSI_AREAS.csv 86159 2022-04-13 15:14:00
8 SDWA_REF_CODE_VALUES.csv 117171 2022-04-13 15:14:00
9 SDWA_SERVICE_AREAS.csv 18901420 2022-04-13 15:15:00
10 SDWA_SITE_VISITS.csv 336423869 2022-04-13 15:17:00
11 SDWA_VIOLATIONS_ENFORCEMENT.csv 3208856416 2022-04-13 15:41:00
<- as.Date(unzip("data/sdwa.zip", list = TRUE)$Date[11]) end_date
We load the violations table and filter to violations that end later than April 13, 2020, two years before the the most current data was released.
<- end_date - 365*2
start_date <- vroom("data/SDWA_VIOLATIONS_ENFORCEMENT.csv", # read data
violators col_types = cols(.default = "c")) %>%
filter(IS_HEALTH_BASED_IND == "Y") %>% # select only health-based violations
mutate( #format dates as dates
start = as.Date(COMPL_PER_BEGIN_DATE,
format = "%m/%d/%Y"),
end = pmin(as.Date(CALCULATED_RTC_DATE,
format = "%m/%d/%Y"),
# set end date to most current report date end_date))
<- violators %>%
violators mutate(
violation_duration = end - start
)
<- violators %>%
violators filter(end >= start_date) %>%
filter(violation_duration >= 30) %>%
distinct(PWSID)
First we download and compress the TEMM layer for quick access in later parts of the workflow
download.file(url = "https://www.hydroshare.org/django_irods/rest_download/6f3386bb4bc945028391cfabf1ea252e/data/contents/temm_layer_v1.0.0/temm.geojson/?url_download=True&zipped=False&aggregation=False", destfile = "data/temm.geojson")
<- sf::read_sf("data/temm.geojson")
temm qsave(temm, file = "data/temm.qs")
Now we load the file, and filter for only the states with comprehensive Tier 1 availability (AZ, CA, CT, KS, NJ, NM, OK, PA, TX, WA) as well as Utah for comparison. We also load in Utah’s data for Culinwary Water Service Areas, filtering for Community Water Systems, which was not included in the original TEMM layer but has been created by Utah DWR, to compare performance between TEMM estimation methods and Tier 1 if possible.
<- qread("data/temm.qs")
temm <- c("AZ",
states "CA",
"CT",
"KS",
"NJ",
"NM",
"OK",
"PA",
"TX",
"UT",
"WA")
<- sf::read_sf("https://services.arcgis.com/ZzrwjTRez6FJiOq4/arcgis/rest/services/CulinaryWaterServiceAreas/FeatureServer/0/query?where=1%3D1&objectIds=&time=&geometry=&geometryType=esriGeometryEnvelope&inSR=&spatialRel=esriSpatialRelIntersects&resultType=none&distance=0.0&units=esriSRUnit_Meter&returnGeodetic=false&outFields=*&returnGeometry=true&returnCentroid=false&featureEncoding=esriDefault&multipatchOption=xyFootprint&maxAllowableOffset=&geometryPrecision=&outSR=4326&defaultSR=&datumTransformation=&applyVCSProjection=false&returnIdsOnly=false&returnUniqueIdsOnly=false&returnCountOnly=false&returnExtentOnly=false&returnQueryGeometry=false&returnDistinctValues=false&cacheHint=false&orderByFields=&groupByFieldsForStatistics=&outStatistics=&having=&resultOffset=&resultRecordCount=&returnZ=false&returnM=false&returnExceededLimitFeatures=true&quantizationParameters=&sqlFormat=none&f=pgeojson") %>% filter(SYSTEMTYPE == "C")
utah
<- temm %>% filter(
boundaries %in% states
state_code %>% select(-service_area_type_code) )
First we download and unzip the data
download.file(url = "https://static-data-screeningtool.geoplatform.gov/data-pipeline/data/score/shapefile/usa.zip", destfile = "data/j40.zip")
unzip("data/j40.zip", exdir = "data/j40")
Then we load it and the data dictionary
<- sf::read_sf("data/j40/usa.shp")
j40 <- vroom("data/j40/columns.csv") j40_dict
Rows: 110 Columns: 3
── Column specification ────────────────────────────────────────────────────────
Delimiter: ","
chr (3): shapefile_column, column_name, column_description
ℹ Use `spec()` to retrieve the full column specification for this data.
ℹ Specify the column types or set `show_col_types = FALSE` to quiet this message.
<- boundaries %>%
v filter(pwsid %in% violators$PWSID)
First we spatially intersect the set of CWS boundaries with the threshold period of health-based violations to the CEJST version of the 2010 U.S. Census Tracts. We also do Utah separately. Caveats:
I am not setting a threshold for how large an overlap between a census tract and a utility needs to be. A census tract may have only a small portion covered by a utility and we are counting it. There are two scenarios:
A relatively large system only serves part of a tract on its periphery
A small system with a tier 1 boundary like a mobile home park or golf course community lies completely within a tract much larger than it
I have not quantified this yet
<- c("Arizona",
states2 "California",
"Connecticut",
"Kansas",
"New Jersey",
"New Mexico",
"Oklahoma",
"Pennsylvania",
"Texas",
"Utah",
"Washington")
<- j40 %>% filter(
j40 %in% states2
SF
)
<- v %>% filter(state_code == states[1])
v1 <- j40 %>% filter(SF == states2[1])
j1
<- st_intersection(v1,j1) v_j40
although coordinates are longitude/latitude, st_intersection assumes that they are planar
Warning: attribute variables are assumed to be spatially constant throughout all
geometries
for (i in 2:length(states)){
<- v %>% filter(state_code == states[i])
v1 <- j40 %>% filter(SF == states2[i])
j1 <- st_intersection(v1,j1)
v_j40_1 <- bind_rows(v_j40,v_j40_1)
v_j40 print(paste0(i))
}
although coordinates are longitude/latitude, st_intersection assumes that they are planar
Warning: attribute variables are assumed to be spatially constant throughout all
geometries
[1] "2"
although coordinates are longitude/latitude, st_intersection assumes that they are planar
Warning: attribute variables are assumed to be spatially constant throughout all
geometries
[1] "3"
although coordinates are longitude/latitude, st_intersection assumes that they are planar
Warning: attribute variables are assumed to be spatially constant throughout all
geometries
[1] "4"
although coordinates are longitude/latitude, st_intersection assumes that they are planar
Warning: attribute variables are assumed to be spatially constant throughout all
geometries
[1] "5"
although coordinates are longitude/latitude, st_intersection assumes that they are planar
Warning: attribute variables are assumed to be spatially constant throughout all
geometries
[1] "6"
although coordinates are longitude/latitude, st_intersection assumes that they are planar
Warning: attribute variables are assumed to be spatially constant throughout all
geometries
[1] "7"
although coordinates are longitude/latitude, st_intersection assumes that they are planar
Warning: attribute variables are assumed to be spatially constant throughout all
geometries
[1] "8"
although coordinates are longitude/latitude, st_intersection assumes that they are planar
Warning: attribute variables are assumed to be spatially constant throughout all
geometries
[1] "9"
although coordinates are longitude/latitude, st_intersection assumes that they are planar
Warning: attribute variables are assumed to be spatially constant throughout all
geometries
[1] "10"
although coordinates are longitude/latitude, st_intersection assumes that they are planar
Warning: attribute variables are assumed to be spatially constant throughout all
geometries
[1] "11"
<- utah %>% filter(DWSYSNUM %in% v$pwsid)
v_utah <- j40 %>% filter(SF == "Utah")
j_utah
<- st_intersection(v_utah,j_utah) v_j40_utah
although coordinates are longitude/latitude, st_intersection assumes that they are planar
Warning: attribute variables are assumed to be spatially constant throughout all
geometries
qsave(v_j40,"data/viol_pws_j40.qs")
qsave(v_j40_utah, "data/viol_pws_j40_utah.qs")
Then, we need to construct indicator as framed by CEJST, meaning we assign to each Census tract a binary indicator as to whether it intersects a violating water system as well as meeting the socioeconomic threshold set by the CEJST (>65th percentile for percentage of population living on incomes <200% of the Federal Poverty line and with 80% or more of adults >15 not enrolled in higher education). We also only allow for Tier 1 and Tier 2a matches, removing all Tier 2b and Tier 3 matches.
<- c("Arizona",
states2 "California",
"Connecticut",
"Kansas",
"New Jersey",
"New Mexico",
"Oklahoma",
"Pennsylvania",
"Texas",
"Utah",
"Washington")
<- j40 %>% filter(
j40 %in% states2
SF
)
<- qread("data/viol_pws_j40_utah.qs")
v_j40_utah <- qread("data/viol_pws_j40.qs") %>% filter(tier == "Tier 1" | tier == "Tier 2a")
v_j40
<- v_j40 %>% filter(SF != "Utah")
v_j40
<- j40 %>%
j40_dw mutate(
DW = case_when(GEOID10 %in% v_j40$GEOID10 ~ "SDWA Violation Present",
TRUE ~ "SDWA Violation Not Present"), # DW indicator with Tier 2 Utah
DW_ut1 = case_when( ((GEOID10 %in% v_j40$GEOID10 & SF != "Utah") | (GEOID10 %in% v_j40_utah$GEOID10)) ~ "SDWA Violation Present",
TRUE ~ "SDWA Violation Not Present") # DW indicator with Tier 1 Utah
)
<- j40_dw %>%
j40_dw mutate(
dw_disadv = (DW == "SDWA Violation Present" & M_EBSI == 1),
dw_disadv_ut1 = (DW_ut1 == "SDWA Violation Present" & M_EBSI == 1)
)
counts
Tract counts in selected states by Drinking Water indicator and current CEJST disadvantage status
<- j40_dw %>%
j40_dw mutate(
CEJST_disadv = case_when(
== 1 ~ "CEJST disadvantaged",
SM_C TRUE ~ "Not CEJST disadvantaged"
), CEJST_income_educ_threshold = case_when(
== 1 ~ "CEJST 'low' inc/edu",
M_EBSI TRUE ~ "CEJST 'high' inc/edu"
)
)<- j40_dw %>% st_drop_geometry() %>%
x tabyl(DW_ut1,CEJST_disadv) %>%
adorn_totals(where = c("row","col")) %>%
adorn_percentages("col") %>%
adorn_pct_formatting() %>%
adorn_ns()
kable(x)
DW_ut1 | CEJST disadvantaged | Not CEJST disadvantaged | Total |
---|---|---|---|
SDWA Violation Not Present | 88.1% (7135) | 89.2% (15324) | 88.9% (22459) |
SDWA Violation Present | 11.9% (961) | 10.8% (1850) | 11.1% (2811) |
Total | 100.0% (8096) | 100.0% (17174) | 100.0% (25270) |
Tract counts in selected states by Drinking Water indicator and current CEJST disadvantage status by State
<- j40_dw %>%
j40_dw mutate(
CEJST_disadv = case_when(
== 1 ~ "CEJST disadvantaged",
SM_C TRUE ~ "Not CEJST disadvantaged"
), CEJST_income_educ_threshold = case_when(
== 1 ~ "CEJST 'low' inc/edu",
M_EBSI TRUE ~ "CEJST 'high' inc/edu"
)
)%>% st_drop_geometry() %>%
j40_dw tabyl(DW_ut1,CEJST_disadv,SF) %>%
adorn_totals(where = c("row","col")) %>%
adorn_percentages("col") %>%
adorn_pct_formatting() %>%
adorn_ns() %>%
walk2(names(.), ~ print(kable(.x, caption = .y)))
DW_ut1 | CEJST disadvantaged | Not CEJST disadvantaged | Total |
---|---|---|---|
SDWA Violation Not Present | 92.9% (459) | 96.3% (994) | 95.2% (1453) |
SDWA Violation Present | 7.1% (35) | 3.7% (38) | 4.8% (73) |
Total | 100.0% (494) | 100.0% (1032) | 100.0% (1526) |
DW_ut1 | CEJST disadvantaged | Not CEJST disadvantaged | Total |
---|---|---|---|
SDWA Violation Not Present | 96.8% (2815) | 98.0% (5049) | 97.6% (7864) |
SDWA Violation Present | 3.2% (92) | 2.0% (101) | 2.4% (193) |
Total | 100.0% (2907) | 100.0% (5150) | 100.0% (8057) |
DW_ut1 | CEJST disadvantaged | Not CEJST disadvantaged | Total |
---|---|---|---|
SDWA Violation Not Present | 96.5% (165) | 95.8% (634) | 95.9% (799) |
SDWA Violation Present | 3.5% (6) | 4.2% (28) | 4.1% (34) |
Total | 100.0% (171) | 100.0% (662) | 100.0% (833) |
DW_ut1 | CEJST disadvantaged | Not CEJST disadvantaged | Total |
---|---|---|---|
SDWA Violation Not Present | 90.2% (184) | 81.3% (460) | 83.6% (644) |
SDWA Violation Present | 9.8% (20) | 18.7% (106) | 16.4% (126) |
Total | 100.0% (204) | 100.0% (566) | 100.0% (770) |
DW_ut1 | CEJST disadvantaged | Not CEJST disadvantaged | Total |
---|---|---|---|
SDWA Violation Not Present | 61.2% (301) | 68.1% (1033) | 66.4% (1334) |
SDWA Violation Present | 38.8% (191) | 31.9% (485) | 33.6% (676) |
Total | 100.0% (492) | 100.0% (1518) | 100.0% (2010) |
DW_ut1 | CEJST disadvantaged | Not CEJST disadvantaged | Total |
---|---|---|---|
SDWA Violation Not Present | 73.0% (165) | 89.4% (244) | 82.0% (409) |
SDWA Violation Present | 27.0% (61) | 10.6% (29) | 18.0% (90) |
Total | 100.0% (226) | 100.0% (273) | 100.0% (499) |
DW_ut1 | CEJST disadvantaged | Not CEJST disadvantaged | Total |
---|---|---|---|
SDWA Violation Not Present | 66.5% (300) | 78.0% (464) | 73.0% (764) |
SDWA Violation Present | 33.5% (151) | 22.0% (131) | 27.0% (282) |
Total | 100.0% (451) | 100.0% (595) | 100.0% (1046) |
DW_ut1 | CEJST disadvantaged | Not CEJST disadvantaged | Total |
---|---|---|---|
SDWA Violation Not Present | 87.4% (653) | 85.1% (2102) | 85.6% (2755) |
SDWA Violation Present | 12.6% (94) | 14.9% (369) | 14.4% (463) |
Total | 100.0% (747) | 100.0% (2471) | 100.0% (3218) |
DW_ut1 | CEJST disadvantaged | Not CEJST disadvantaged | Total |
---|---|---|---|
SDWA Violation Not Present | 85.8% (1776) | 86.0% (2748) | 85.9% (4524) |
SDWA Violation Present | 14.2% (293) | 14.0% (448) | 14.1% (741) |
Total | 100.0% (2069) | 100.0% (3196) | 100.0% (5265) |
DW_ut1 | CEJST disadvantaged | Not CEJST disadvantaged | Total |
---|---|---|---|
SDWA Violation Not Present | 94.7% (72) | 86.3% (442) | 87.4% (514) |
SDWA Violation Present | 5.3% (4) | 13.7% (70) | 12.6% (74) |
Total | 100.0% (76) | 100.0% (512) | 100.0% (588) |
DW_ut1 | CEJST disadvantaged | Not CEJST disadvantaged | Total |
---|---|---|---|
SDWA Violation Not Present | 94.6% (245) | 96.2% (1154) | 96.0% (1399) |
SDWA Violation Present | 5.4% (14) | 3.8% (45) | 4.0% (59) |
Total | 100.0% (259) | 100.0% (1199) | 100.0% (1458) |
Tract counts in selected states by Drinking Water indicator and current CEJST disadvantage status, by the CEJST non-student low-income socioeconomic indicator
<-j40_dw %>% st_drop_geometry() %>%
xtabyl(DW_ut1,CEJST_disadv,CEJST_income_educ_threshold) %>%
adorn_totals(where = c("row","col")) %>%
adorn_percentages("col") %>%
adorn_pct_formatting() %>%
adorn_ns() %>%
walk2(names(.), ~ print(kable(.x, caption = .y)))
DW_ut1 | CEJST disadvantaged | Not CEJST disadvantaged | Total |
---|---|---|---|
SDWA Violation Not Present | 91.2% (719) | 89.2% (14794) | 89.3% (15513) |
SDWA Violation Present | 8.8% (69) | 10.8% (1783) | 10.7% (1852) |
Total | 100.0% (788) | 100.0% (16577) | 100.0% (17365) |
DW_ut1 | CEJST disadvantaged | Not CEJST disadvantaged | Total |
---|---|---|---|
SDWA Violation Not Present | 87.8% (6416) | 88.8% (530) | 87.9% (6946) |
SDWA Violation Present | 12.2% (892) | 11.2% (67) | 12.1% (959) |
Total | 100.0% (7308) | 100.0% (597) | 100.0% (7905) |
<- j40_dw %>%
pop_by_tract_type ungroup() %>%
group_by(CEJST_disadv,dw_disadv_ut1) %>%
summarise(population = sum(TPF,na.rm=TRUE)) %>% ungroup()
`summarise()` has grouped output by 'CEJST_disadv'. You can override using the
`.groups` argument.
although coordinates are longitude/latitude, st_union assumes that they are
planar
although coordinates are longitude/latitude, st_union assumes that they are
planar
although coordinates are longitude/latitude, st_union assumes that they are
planar
although coordinates are longitude/latitude, st_union assumes that they are
planar
<- j40_dw %>%
j40_types mutate(cat1 = case_when(
& CEJST_disadv == "CEJST disadvantaged" ~ "DW EJ indicator + Current CEJST",
dw_disadv_ut1 !dw_disadv_ut1 & CEJST_disadv == "CEJST disadvantaged" ~ "No DW EJ indicator + Current CEJST",
!dw_disadv_ut1 & CEJST_disadv == "Not CEJST disadvantaged" ~ "No DW EJ indicator + Not Current CEJST",
& CEJST_disadv == "Not CEJST disadvantaged" ~ "DW EJ indicator + Not Current CEJST"
dw_disadv_ut1
),cat2 = case_when(
=="SDWA Violation Present" & CEJST_disadv == "CEJST disadvantaged" ~ "DW violation + Current CEJST",
DW_ut1=="SDWA Violation Not Present" & CEJST_disadv == "CEJST disadvantaged" ~ "No DW violation + Current CEJST",
DW_ut1=="SDWA Violation Not Present" & CEJST_disadv == "Not CEJST disadvantaged" ~ "No DW violation + Not Current CEJST",
DW_ut1=="SDWA Violation Present" & CEJST_disadv == "Not CEJST disadvantaged" ~ "DW violation + Not Current CEJST"
DW_ut1%>% ungroup()
))
<- j40_types %>% group_by(cat1) %>% summarise(pop=sum(TPF,na.rm=TRUE)) j40_types_1
although coordinates are longitude/latitude, st_union assumes that they are planar
although coordinates are longitude/latitude, st_union assumes that they are planar
although coordinates are longitude/latitude, st_union assumes that they are planar
although coordinates are longitude/latitude, st_union assumes that they are planar
<- j40_types %>% group_by(cat2) %>% summarise(pop=sum(TPF,na.rm=TRUE)) j40_types_2
although coordinates are longitude/latitude, st_union assumes that they are planar
although coordinates are longitude/latitude, st_union assumes that they are planar
although coordinates are longitude/latitude, st_union assumes that they are planar
although coordinates are longitude/latitude, st_union assumes that they are planar
<- pop_by_tract_type %>%
table st_drop_geometry() %>%
mutate(pop_millions = population/1000000) %>%
select(-population) %>%
pivot_wider(names_from=dw_disadv_ut1, values_from = pop_millions) %>%
rename(`DW Violation + low income`=`TRUE`,
`No DW Violation or not low income`=`FALSE`)
kable(table)
CEJST_disadv | No DW Violation or not low income | DW Violation + low income |
---|---|---|
CEJST disadvantaged | 32.18822 | 3.655508 |
Not CEJST disadvantaged | 82.27428 | 0.338452 |
Map 1: Census Tracts categorized by drinking water violations and current CEJST status
mapviewOptions(fgb = TRUE)
<-mapview::mapview(j40_types_2, zcol="cat2", layer.name="Tract Category")
m1::mapshot(m1,url="map1.html")
mapview m1
Map 2: Census Tracts categorized by provisional drinking water EJ indicator (drinking water violation + low income) and current CEJST status
mapviewOptions(fgb = TRUE)
<-mapview::mapview(j40_types_1, zcol="cat1", layer.name="Tract Category")
m2::mapshot(m2,url="map2.html")
mapview m2
library(tidycensus)
census_api_key("b25f8b1b7bf10561c9cbc3a20a4d2572677f1f05")
options(tigris_use_cache = TRUE)
<- tidycensus::get_acs() tr
Will discuss results in detail here.
959 “poor” tract/communities across the 11 states were identified to have safe drinking water violations, 67 of which were not previously identified as disadvantaged in the CEJST