tidycensus::census_api_key(Sys.getenv("CENSUS_API")) #Census API Key
google_api_key <- Sys.getenv("GOOGLE_API") #Google API
#Getting Census Block Group Boundary
bg <- suppressMessages(
tidycensus::get_acs(geography = "block group", # or "block", "tract", "county", "state" etc.
state = "GA",
county = c("Gwinnett"),
variables = c(hhincome = 'B19013_001'),
year = 2023,
survey = "acs5", # we typically use American Community Survey 5-year estimates
geometry = TRUE, # we need an sf object
output = "wide") # wide vs. long
)
#City of Duluth boundary
duluth <- tigris::places('GA') %>% filter(NAME == 'Duluth')
#Get block groups intersecting with the City of Duluth boundary
bg_duluth <- bg[duluth,]
#Check if city of choice is too big for assignment
dim(bg_duluth) #dim: 31x5 (31 block groups)
## [1] 31 5
#Function: Get XY coordinates and radius
getXYRadius <- function(polygon, gcs_id, pcs_id){
#Transform the CRS to PCS
if (st_crs(polygon) != st_crs(pcs_id)){
polygon <- polygon %>% st_transform(pcs_id)
}
#Get bounding box of a given polygon
bb <- st_bbox(polygon)
#Get XY coordinates of any one corner of the bounding box.
bb_corner <- st_point(c(bb[1], bb[2])) %>% st_sfc(crs = pcs_id)
#Get centroid of the bb
bb_center <- bb %>% st_as_sfc() %>% st_centroid()
#Get the distance between bb_center and bb_corner
r <- st_distance(bb_corner, bb_center)
#Convert the CRS of centroid to GCS
bb_center <- bb_center %>% st_transform(gcs_id)
#Get longitude and latitude
xy <- bb_center %>% st_coordinates() %>% as.vector()
lon_lat_radius <- data.frame(x = xy[1],
y = xy[2],
r = r)
return(lon_lat_radius)
}
#Define EPSG codes for GCS (WGS 84) and PCS (WGS 84 / UTM zone 16N)
gcs_id <- 4326 #georgia's gcs ID: 4326
pcs_id <- 32616 #georgia's pcs ID: 32616
#Pre-allocate a data frame. Results will fill this data frame
bg_duluth_xyr <- data.frame(x = numeric(nrow(bg_duluth)),
y = NA,
r = NA)
#Do a for-loop
for (i in 1:nrow(bg_duluth)){
bg_duluth_xyr[i,] <- bg_duluth[i, ] %>%
getXYRadius(gcs_id = gcs_id,
pcs_id = pcs_id)
}
tmap_mode('view')
## โน tmap mode set to "view".
bg_duluth_xyr %>%
#Convert the data frame into an sf object
st_as_sf(coords = c("x", "y"), crs = st_crs(bg_duluth)) %>%
#Draw a buffer centered at the centroid of block group polygons, with buffer distance as the radius we calculated in bg_duluth_xyr
st_buffer(dist = .$r) %>%
#Display buffer
tm_shape(.) + tm_polygons(alpha = 0.2, col = '#84a5db', border.col = '#84a5db', lwd = 1.2) +
#Display the original Duluth block group polygon
tm_shape(bg_duluth) + tm_borders(col= '#05327d', lwd = 1.5)
##
## โโ tmap v3 code detected โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
## [v3->v4] `tm_polygons()`: use 'fill' for the fill color of polygons/symbols
## (instead of 'col'), and 'col' for the outlines (instead of 'border.col').[v3->v4] `tm_polygons()`: use `fill_alpha` instead of `alpha`.
nearbySearch <- function(lat, lon, radius, types_vec, fieldmask_vec, google_api_key){
endpoint <- "https://places.googleapis.com/v1/places:searchNearby" #Nearby Search endpoint
body <- list(
includedTypes = as.list(types_vec),
locationRestriction = list(
circle = list(
center = list(latitude = lat, longitude = lon),
radius = radius
)
),
rankPreference = "DISTANCE"
)
resp <- POST(
endpoint,
add_headers(
"Content-Type" = "application/json",
"X-Goog-Api-Key" = google_api_key,
"X-Goog-FieldMask" = paste(fieldmask_vec, collapse = ",")),
body = body,
encode = "json"
)
data <- content(resp, as="text") %>%
jsonlite::fromJSON(flatten = T) %>%
as.data.frame()
if (nrow(data) == 20){
print("WARNING: The response has 20 rows! Consider using a smaller spatial unit.")
}
return(data)
}
#Pre-allocate list
data_list <- vector("list", nrow(bg_duluth_xyr))
for (i in seq_len(nrow(bg_duluth_xyr))) {
data_list[[i]] <- nearbySearch(
lat = bg_duluth_xyr$y[i],
lon = bg_duluth_xyr$x[i],
radius = bg_duluth_xyr$r[i],
types_vec = c("cafe", "brunch_restaurant"),
fieldmask_vec = c("places.id",
"places.displayName",
"places.formattedAddress",
"places.location",
"places.types",
"places.priceLevel",
"places.rating",
"places.userRatingCount"),
google_api_key = Sys.getenv("GOOGLE_API")
)
Sys.sleep(1)
}
#Combine all data frames
data_all <- dplyr::bind_rows(data_list)
saveRDS(data_all, here('duluth_google_poi_data.rds'))
#Read in the saved RDS file
poi <- read_rds("duluth_google_poi_data.rds")
#City of Duluth boundary
duluth <- tigris::places('GA') %>% filter(NAME == 'Duluth')
## Retrieving data for the year 2024
#Convert the data to an sf object using XY coordinates
poi_sf <- poi %>%
rename(x = places.location.longitude, y = places.location.latitude) %>%
filter(!is.na(x) & !is.na(y)) %>%
st_as_sf(coords = c("x", "y"), crs = 4326)
#Interactive map of both the POIs and the City of Duluth boundary
tm_shape(duluth) +
tm_borders(lwd = 1.3, col = "#545454") +
tm_fill(alpha=0.4, col = "#bababa") +
tm_shape(poi_sf) +
tm_dots(col = "places.rating", title.col="Rating", breaks = c(2,3,4,5), border.col = "#545454", lwd = 1, shape = 21, palette = c("#e86464", "#edc15a", "#8fc989"),
size = "places.userRatingCount", scale=2,
popup.vars = c("Name" = "places.displayName.text",
"Price Level" = "places.priceLevel",
"Rating" = "places.rating",
"Rating Count" = "places.userRatingCount")) +
tm_title("Interactive Map of Rating and Rating Count of Brunch Restaurants and Cafes in Duluth, GA")
##
## โโ tmap v3 code detected โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
## [v3->v4] `tm_polygons()`: use `fill_alpha` instead of `alpha`.
## [v3->v4] `tm_dots()`: migrate the argument(s) related to the legend of the
## visual variable `fill` namely 'title.col' (rename to 'title') to 'fill.legend =
## tm_legend(<HERE>)'
## [v3->v4] `tm_tm_dots()`: migrate the argument(s) related to the scale of the
## visual variable `size` namely 'scale' (rename to 'values.scale') to size.scale
## = tm_scale_continuous(<HERE>).
## โน For small multiples, specify a 'tm_scale_' for each multiple, and put them in
## a list: 'size.scale = list(<scale1>, <scale2>, ...)'
I chose the City of Duluth in Georgia.
The two place types I selected are cafes and brunch restaurants.
There are 161 rows in my dataset.
Regarding spatial patterns, most cafes and brunch restaurants locate at the road-side of more major roads and they mostly cluster together.
I also noticed that many of the places with high user rating counts also have high ratings.