#Loading tigris data for the state of Wisconsin
wi <- places(
state = "WI",
year = 2024,
class = 'sf'
)
#Filtering data to Appleton's city limits
appleton <- wi |>
filter(NAME == "Appleton")
#Plotting Appleton for sanity check
plot(st_geometry(appleton))
#Appleton has a bunch of disjointed segments, so the block groups approach should be better than the fishnet approach.
wi_bg <- block_groups(
state = "WI",
year = 2024,
class = "sf"
)
appleton_bg <- wi_bg |>
st_filter(appleton)
#Plotting census block groups for sanity check. Some of the census block groups are very sparse, but should be good enough.
plot(st_geometry(appleton_bg))
plot(st_geometry(appleton), add = TRUE, border = "red", lwd = 2)
#Rewriting the function from the last lab session to have an efficient way of getting the three parameteres required for API queries.
getXYRadius <- function(polygon, gcs_id, pcs_id){
#Transforming into projected coordinate system to get distances correctly
if (st_crs(polygon) != st_crs(pcs_id)){
polygon <- polygon %>% st_transform(pcs_id)
}
#Using bounding boxes for each polygon to ensure that the entire geographic extent is covered.
bb <- st_bbox(polygon)
bb_corner <- st_point(c(bb[1], bb[2])) %>% st_sfc(crs = pcs_id)
#Getting center coordinates as a dot type geography
bb_center <- bb %>% st_as_sfc() %>% st_centroid()
#Calculating the radius for each polygon's corresponding circle
r <- st_distance(bb_center, bb_corner)
#Transforming to geographic coordinate system as Google API needs degrees
bb_center <- bb_center %>% st_transform(gcs_id)
xy <- bb_center %>% st_coordinates() %>% as.vector()
#Generating final 3 values: latitude, longitude, and radius
lon_lat_radius <- data.frame(x = xy[1],
y = xy[2],
r = r)
return(lon_lat_radius)
}
#Declaring the GCS and PCS used for this task.
gcs_id <- 4269
pcs_id <- 26916
#Initiating empty daframe with length equal to the number of census blocks
bg_appleton_xyr <- data.frame(x = numeric(nrow(appleton_bg)),
y = NA,
r = NA)
#Populating the dataframe with the three values returned by running the getXYRadius function on each census block
for (i in 1:nrow(appleton_bg)){
bg_appleton_xyr[i,] <- appleton_bg[i, ] %>%
getXYRadius(gcs_id = gcs_id,
pcs_id = pcs_id)
}
tmap_mode("view")
# Map checking whether the locationRestriction circles provide full coverage of Appleton. Some circles are huge, but it should be okay as most of those areas are farmland.
bg_appleton_xyr %>%
st_as_sf(coords = c("x", "y"), crs = gcs_id) %>%
st_transform(pcs_id) %>%
st_buffer(dist = .$r) %>%
tm_shape() +
tm_polygons(fill = "red", fill_alpha = 0.1) +
tm_shape(appleton) +
tm_borders(col = "blue", line_alpha = 0.8)
#Rewriting nearbySearch function from the last lab to get API results.
nearbySearch <- function(lat, lon, radius, types_vec, fieldmask_vec, google_api_key){
endpoint <- 'https://places.googleapis.com/v1/places:searchNearby'
body <- list(
includedTypes = as.list(types_vec),
locationRestriction = list(
circle = list(
center = list(latitude = lat, longitude = lon),
radius = radius
) #Circle values will be sourced from the entries in the dataframe.
),
rankPreference = "DISTANCE" #To prioritize closer results in case more than 20 results are obtained
)
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"
) #Sending the post query with the parameters defined above. Results are obtained as JSON block.
data <- content(resp, as="text") %>%
jsonlite::fromJSON(flatten = T) %>%
as.data.frame() #Converting JSON to a dataframe for readability
if (nrow(data) == 20){
print("WARNING: The response has 20 rows.") #Warning to indicate that some results may be truncated
}
return(data)
}
google_api_key <- Sys.getenv("GOOGLE_API_KEY")
data <- vector("list", nrow(bg_appleton_xyr)) #Initializing an empty vector
#For-loop runs nearbySearch on every entry of the x, y, radius dataframe to populate the empty vector
for (i in seq_len(nrow(bg_appleton_xyr))) {
data[[i]] <- nearbySearch(
lat = bg_appleton_xyr$y[i],
lon = bg_appleton_xyr$x[i],
radius = bg_appleton_xyr$r[i],
#Two selected POIs: Fast Food Restaurants and Discount Stores
types_vec = c("fast_food_restaurant", "discount_store"),
#Field masks, as required by the problem
fieldmask_vec = c(
"places.id",
"places.displayName",
"places.formattedAddress",
"places.location",
"places.types",
"places.priceLevel",
"places.rating",
"places.userRatingCount"
),
google_api_key = google_api_key
)
#Half-second buffer between queries to limit API call frequency
Sys.sleep(0.5)
}
## [1] "WARNING: The response has 20 rows."
#Combining all dataframes obtained into one single dataframe. Removing duplicates from overlapping circles.
dataStack <- dplyr::bind_rows(data) |>
distinct(places.id, .keep_all = TRUE)
write_rds(dataStack, 'appletonData.rds')
#Converting data into sf-type geographic data based on latitudes and longitudes for mapping
sf_data <- st_as_sf(dataStack, coords = c("places.location.longitude", "places.location.latitude"),
crs = gcs_id)
#places.types returned a list with numerous types that match each result. This function creates a new column called primary_place that sorts each entry into either fast food or discount store based of whether each type is contained in the list. This makes mapping easier.
sf_data$primary_type <- sapply(
sf_data$places.types,
function(x) {
if ("fast_food_restaurant" %in% x) {
"Fast Food Restaurant"
} else if ("discount_store" %in% x) {
"Discount Store"
} else {
"Other"
}
}
)
tmap_mode("view")
#Plotting official Appleton city limits.
tm_shape(appleton) +
tm_borders(col = "maroon", lwd = 1.5) +
tm_add_legend(
type = "lines",
labels = "Appleton City Limits",
col = "maroon",
lwd = 1.5,
position = 'bottom'
) +
# Filtering data by primary type to plot fast food restaurants. The dot color is on a linear scale based on rating. The dot sizes are scaled by the square root of the number of reviews to avoid visual dominance by restaurants with the most reviews.
tm_shape(sf_data[sf_data$primary_type == "Fast Food Restaurant", ]) +
tm_dots(
fill = "places.rating",
fill.scale = tm_scale_continuous(values = "Greens"),
fill.legend = tm_legend(
title = "Fast Food Rating",
title.size = 0.1,
width = 0.75,
height = 10.5
),
size = "places.userRatingCount",
size.legend = tm_legend_hide(),
size.scale = tm_scale_continuous_sqrt(values.scale = 1),
col = "black",
shape = 21,
popup.vars = c(
"Name" = "places.displayName.text",
"Rating" = "places.rating",
"Rating Count" = "places.userRatingCount",
"Type" = "primary_type"
)
) +
# Filtering data by primary type to plot fast food restaurants. The dot color is on a linear scale based on rating. The dot sizes are scaled by the square root of the number of reviews to avoid visual dominance by stores with the most reviews.
tm_shape(sf_data[sf_data$primary_type == "Discount Store", ]) +
tm_dots(
fill = "places.rating",
fill.scale = tm_scale_continuous(values = "Blues"),
fill.legend = tm_legend(
"Disc. Store Rating",
title.size = 0.1,
width = 0.75,
height = 10.5
),
size = "places.userRatingCount",
size.legend = tm_legend_hide(),
size.scale = tm_scale_continuous_sqrt(values.scale = 1),
col = "black",
shape = 21,
popup.vars = c(
"Name" = "places.displayName.text",
"Rating" = "places.rating",
"Rating Count" = "places.userRatingCount",
"Type" = "primary_type"
)
) +
tm_layout(
legend.outside = TRUE,
legend.outside.position = "bottom",
legend.stack = "vertical",
legend.text.size = 0.7,
legend.title.size = 0.8
)
I chose the city of Appleton, Wisconsin. It is a mid-sized city north of Milwaukee and south of Green Bay.
I selected fast food restaurants and discount stores, as I wanted to see if there is any spatial correlation in their locations.
The final dataset contains 115 rows.
I was hoping to see visual clusters of the two points of interest in areas of lower income, but they only seem to be patterned around general commercial areas.
There are streaks of both types of locations along major streets, which makes sense as discount stores are likely to be inside strip malls and fast food locations are likely to have drive-throughs beside popular roads.
Both types of locations are mostly absent from the downtown area of the city, which, from personal experience, is dominated by more independent stores and bars.