library(tidycensus)
library(tigris)
## To enable caching of data, set `options(tigris_use_cache = TRUE)`
## in your R script or .Rprofile.
library(sf)
## Linking to GEOS 3.13.1, GDAL 3.11.0, PROJ 9.6.0; sf_use_s2() is TRUE
library(tmap)
library(httr)
library(jsonlite)
library(dplyr)
##
## Attaching package: 'dplyr'
## The following objects are masked from 'package:stats':
##
## filter, lag
## The following objects are masked from 'package:base':
##
## intersect, setdiff, setequal, union
library(here)
## here() starts at C:/Intro to UA
library(knitr)
tidycensus::census_api_key(Sys.getenv("CENSUS_API"), install = FALSE)
## To install your API key for use in future sessions, run this function with `install = TRUE`.
library(tidycensus)
library(dplyr)
library(sf)
# Get Orange County block groups quietly
bg <- invisible(
suppressMessages(
suppressWarnings(
tidycensus::get_acs(
geography = "block group",
state = "FL",
county = "Orange",
variables = c(hhincome = "B19013_001"),
year = 2023,
survey = "acs5",
geometry = TRUE,
output = "wide"
)
)
)
)
# Hide tigris progress messages
options(tigris_use_cache = TRUE) # optional, caches downloaded data
options(tigris_class = "sf") # ensures output as sf object
# Get Orlando city boundary
orlando <- tigris::places("FL") %>%
dplyr::filter(NAME == "Orlando")
bg_orlando <- bg[orlando, ] %>%
select(GEOID,
hhincome = hhincomeE)
tmap_mode("view")
## ℹ tmap mode set to "view".
tm_shape(bg_orlando) + tm_borders(lwd = 2) +
tm_shape(orlando) + tm_polygons(col = 'red', alpha = 0.4)
library(sf)
library(dplyr)
library(tmap)
gcs_id <- 4326
pcs_id <- 32617 # UTM Zone 17N
# Function: Get XY coordinates and radius
getXYRadius <- function(polygon, gcs_id, pcs_id){
# Transform the CRS to PCS. WHY?
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. WHY?
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)
}
#We can apply this function to each BG.
# Define EPSG codes for GCS (WGS 84) and PCS (WGS 84 / UTM zone 16N)
gcs_id <- 4326
pcs_id <- 32617
# Pre-allocate a data frame. Results will fill this data frame
bg_orlando_xyr <- data.frame(x = numeric(nrow(bg_orlando)),
y = NA,
r = NA)
# Do a for-loop
for (i in 1:nrow(bg_orlando)){
bg_orlando_xyr[i,] <- bg_orlando[i, ] %>%
getXYRadius(gcs_id = gcs_id,
pcs_id = pcs_id)
}
#Let’s visualize what we’ve just done.
tmap_mode('view')
## ℹ tmap mode set to "view".
## tmap mode set to interactive viewing
bg_orlando_xyr %>%
# Convert the data frame into an sf object
st_as_sf(coords = c("x", "y"), crs = st_crs(bg_orlando)) %>%
# Draw a buffer centered at the centroid of BG polygons.
# The buffer distance is the radius we just calculated
st_buffer(dist = .$r) %>%
# Display this buffer in red
tm_shape(.) + tm_polygons(alpha = 0.1, col = 'red') +
# Display the original polygon in blue
tm_shape(bg_orlando) + tm_borders(col= 'blue')
##
## ── tmap v3 code detected ───────────────────────────────────────────────────────
## [v3->v4] `tm_polygons()`: use `fill_alpha` instead of `alpha`.
Sys.setenv(GOOGLE_API = "AIzaSyDsYeLkn1xMx1Ll2OxJiC2soDyetbXGfx8")
# Retrieve it properly — quotes are mandatory!
google_api_key <- Sys.getenv("GOOGLE_API") # ✅ correct
print(google_api_key)
## [1] "AIzaSyDsYeLkn1xMx1Ll2OxJiC2soDyetbXGfx8"
library(httr)
google_api_key <- Sys.getenv("GOOGLE_API")
lat <- 28.5384 # Atlantic Station
lon <- -81.3789 # Atlantic Station
radius <- 1609.34 # 1 mile in meters
endpoint <- "https://places.googleapis.com/v1/places:searchNearby" # Nearby Search endpoint
body <- list(
includedTypes = list("food_court", "restaurant"),
locationRestriction = list(
circle = list(
center = list(latitude = lat, longitude = lon),
radius = radius
)
)
)
resp <- POST(
endpoint,
add_headers(
"Content-Type" = "application/json",
"X-Goog-Api-Key" = google_api_key,
"X-Goog-FieldMask" = "places.displayName,places.formattedAddress,places.types"),
body = body,
encode = "json"
)
print(resp) # metadata + body
## Response [https://places.googleapis.com/v1/places:searchNearby]
## Date: 2025-09-18 06:18
## Status: 200
## Content-Type: application/json; charset=UTF-8
## Size: 7.39 kB
## {
## "places": [
## {
## "types": [
## "american_restaurant",
## "bar",
## "restaurant",
## "point_of_interest",
## "food",
## "establishment"
## ...
data <- content(resp, as="text")
# Parse JSON into a list and turn it into a data frame
data <- jsonlite::fromJSON(data, flatten = T) %>% as.data.frame()
names(data)
## [1] "places.types" "places.formattedAddress"
## [3] "places.displayName.text" "places.displayName.languageCode"
print(data)
## places.types
## 1 american_restaurant, bar, restaurant, point_of_interest, food, establishment
## 2 restaurant, coffee_shop, cafe, event_venue, bar, point_of_interest, food_store, food, store, establishment
## 3 steak_house, fine_dining_restaurant, restaurant, point_of_interest, food, establishment
## 4 coffee_shop, cafe, breakfast_restaurant, restaurant, point_of_interest, food_store, food, store, establishment
## 5 restaurant, bar, point_of_interest, food, establishment
## 6 restaurant, asian_restaurant, ramen_restaurant, japanese_restaurant, point_of_interest, food, establishment
## 7 restaurant, asian_restaurant, brunch_restaurant, bar, point_of_interest, food, establishment
## 8 mexican_restaurant, restaurant, point_of_interest, food, establishment
## 9 restaurant, brunch_restaurant, coffee_shop, cafe, bakery, breakfast_restaurant, dessert_shop, confectionery, point_of_interest, food_store, food, store, establishment
## 10 fast_food_restaurant, restaurant, point_of_interest, food, establishment
## 11 restaurant, event_venue, bar, point_of_interest, food, establishment
## 12 restaurant, point_of_interest, food, establishment
## 13 bar, restaurant, point_of_interest, food, establishment
## 14 restaurant, point_of_interest, food, establishment
## 15 bar, restaurant, point_of_interest, food, establishment
## 16 mexican_restaurant, restaurant, point_of_interest, food, establishment
## 17 ice_cream_shop, bakery, vegan_restaurant, vegetarian_restaurant, dessert_shop, confectionery, restaurant, point_of_interest, food_store, food, store, establishment
## 18 bar_and_grill, pub, bar, restaurant, point_of_interest, food, establishment
## 19 bar, tourist_attraction, plaza, bar_and_grill, night_club, mexican_restaurant, event_venue, restaurant, point_of_interest, food, establishment
## 20 mexican_restaurant, restaurant, point_of_interest, food, establishment
## places.formattedAddress
## 1 100 S Eola Dr Unit 103, Orlando, FL 32801, USA
## 2 448 N Terry Ave, Orlando, FL 32801, USA
## 3 17 W Church St, Orlando, FL 32801, USA
## 4 47 E Robinson St UNIT 100, Orlando, FL 32801, USA
## 5 211 N Lucerne Cir W, Orlando, FL 32801, USA
## 6 8 N Summerlin Ave, Orlando, FL 32801, USA
## 7 13 S Orange Ave, Orlando, FL 32801, USA
## 8 131 N Orange Ave Unit 103, Orlando, FL 32801, USA
## 9 20 N Orange Ave Ste 102A, Orlando, FL 32801, USA
## 10 57 W Central Blvd, Orlando, FL 32801, USA
## 11 1315 S Orange Ave, Orlando, FL 32806, USA
## 12 66 E Pine St, Orlando, FL 32801, USA
## 13 431 E Central Blvd ste b, Orlando, FL 32801, USA
## 14 100 S Eola Dr SUITE 104, Orlando, FL 32801, USA
## 15 325 S Orange Ave, Orlando, FL 32801, USA
## 16 222 S Orange Ave, Orlando, FL 32801, USA
## 17 420 E Church St Unit 112, Orlando, FL 32801, USA
## 18 25 S Magnolia Ave, Orlando, FL 32801, USA
## 19 25 Wall St, Orlando, FL 32801, USA
## 20 20 E Washington St, Orlando, FL 32801, USA
## places.displayName.text places.displayName.languageCode
## 1 The Stubborn Mule en
## 2 The Monroe en
## 3 Kres Chophouse en
## 4 Craft & Common en
## 5 The Wellborn Restaurant & Bar en
## 6 JINYA Ramen Bar - Thornton Park en
## 7 Thrive Cocktail Lounge & Eatery en
## 8 Tacos My Guey en
## 9 Mecatos Bakery & Café en
## 10 Super Rico Colombian Restaurant & Bar en
## 11 Delaney's Tavern en
## 12 Papi Smash'd Burger en
## 13 World of Beer en
## 14 Eola Lounge en
## 15 The Boheme en
## 16 Solita Tacos & Margaritas en
## 17 The Greenery Creamery en
## 18 Harp & Celt Irish Pub & Restaurant en
## 19 Wall Street Plaza en
## 20 Gringos Locos DOWNTOWN en
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" # WHAT does this parameter do? and WHY is this added?
)
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)
}
nearbySearch(lat = 28.5384,
lon = -81.3789,
radius = 1609.34,
types_vec = c("food_court", "restaurant"),
fieldmask_vec = c("places.displayName",
"places.formattedAddress",
"places.types"),
google_api_key = Sys.getenv(("GOOGLE_API"))
)
## [1] "WARNING: The response has 20 rows! Consider using a smaller spatial unit."
## places.types
## 1 american_restaurant, restaurant, point_of_interest, food, establishment
## 2 seafood_restaurant, restaurant, point_of_interest, food, establishment
## 3 american_restaurant, restaurant, point_of_interest, food, establishment
## 4 restaurant, point_of_interest, food, establishment
## 5 mexican_restaurant, restaurant, point_of_interest, food, establishment
## 6 mexican_restaurant, restaurant, point_of_interest, food, establishment
## 7 restaurant, point_of_interest, food, establishment
## 8 sandwich_shop, restaurant, point_of_interest, food, establishment
## 9 bar, restaurant, point_of_interest, food, establishment
## 10 night_club, bar, restaurant, point_of_interest, food, establishment
## 11 acai_shop, cafe, brazilian_restaurant, restaurant, point_of_interest, food, establishment
## 12 restaurant, point_of_interest, food, establishment
## 13 restaurant, point_of_interest, food, establishment
## 14 wedding_venue, event_venue, restaurant, point_of_interest, food, establishment
## 15 seafood_restaurant, restaurant, point_of_interest, food, establishment
## 16 coffee_shop, breakfast_restaurant, bagel_shop, donut_shop, fast_food_restaurant, cafe, meal_takeaway, bakery, food_store, restaurant, point_of_interest, store, food, establishment
## 17 restaurant, point_of_interest, food, establishment
## 18 restaurant, cafe, point_of_interest, food, establishment
## 19 restaurant, point_of_interest, food, establishment
## 20 deli, sandwich_shop, food_store, restaurant, point_of_interest, store, food, establishment
## places.formattedAddress
## 1 Orlando, FL 32801, USA
## 2 Orlando, FL 32801, USA
## 3 Orlando, FL 32801, USA
## 4 Orlando, FL 32801, USA
## 5 Orlando, FL 32801, USA
## 6 Orlando, FL 32801, USA
## 7 Orlando, FL 32801, USA
## 8 Orlando, FL 32801, USA
## 9 325 S Orange Ave, Orlando, FL 32801, USA
## 10 Grand Bohemian Hotel Orlando, 325 S Orange Ave, Orlando, FL 32801, USA
## 11 300 S Orange Ave #175, Orlando, FL 32801, USA
## 12 400 S Orange Ave, Orlando, FL 32801, USA
## 13 400 S Orange Ave, Orlando, FL 32801, USA
## 14 255 S Orange Ave STE 1800, Orlando, FL 32801, USA
## 15 450 S Orange Ave 3rd Floor, Orlando, FL 32801, USA
## 16 255 S Orange Ave, Orlando, FL 32801, USA
## 17 255 S Orange Ave # 109, Orlando, FL 32801, USA
## 18 450 S Orange Ave, Orlando, FL 32801, USA
## 19 250 S Orange Ave, Orlando, FL 32801, USA
## 20 255 S Orange Ave # 103, Orlando, FL 32801, USA
## places.displayName.text places.displayName.languageCode
## 1 Tex Rex en
## 2 Dockside bar en
## 3 Bubba Shrimp en
## 4 Fish and Fins en
## 5 El progreso en
## 6 Taco cavana es
## 7 Tapia torro en
## 8 Between Breads en
## 9 The Boheme en
## 10 Bosendorfer Lounge en
## 11 Nature's Table Cafe en
## 12 CampofioreENT en
## 13 City Beautiful Cafe en
## 14 Citrus Club en
## 15 Red Lobster Hospitality, LLC. en
## 16 Dunkin' en
## 17 Tikka Bowls and Tacos en
## 18 Nature's Table CNL en
## 19 Latin Square Cuisine en
## 20 New York Deli en
# pre-allocate list
data_list <- vector("list", nrow(bg_orlando_xyr))
for (i in seq_len(nrow(bg_orlando_xyr))) {
data_list[[i]] <- nearbySearch(
lat = bg_orlando_xyr$y[i],
lon = bg_orlando_xyr$x[i],
radius = bg_orlando_xyr$r[i],
types_vec = c("food_court", "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(0.5) # to avoid hitting API rate limits
}
data_all <- dplyr::bind_rows(data_list)
saveRDS(data_all, here('google_poi_data.rds'))
names(data_all)
## [1] "places.id" "places.types"
## [3] "places.formattedAddress" "places.rating"
## [5] "places.priceLevel" "places.userRatingCount"
## [7] "places.location.latitude" "places.location.longitude"
## [9] "places.displayName.text" "places.displayName.languageCode"
data_all_sf <- data_all %>%
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)
tm_shape(data_all_sf) +
tm_dots(col = "places.rating",
size = "places.userRatingCount",
palette = "magma",
popup.vars = c("Name" = "places.displayName.text",
"Rating" = "places.rating",
"Rating Count" = "places.userRatingCount")) +
tm_shape(orlando) +
tm_borders()
##
## ── tmap v3 code detected ───────────────────────────────────────────────────────
## [v3->v4] `tm_tm_dots()`: migrate the argument(s) related to the scale of the
## visual variable `fill` namely 'palette' (rename to 'values') to fill.scale =
## tm_scale(<HERE>).
## Registered S3 method overwritten by 'jsonify':
## method from
## print.json jsonlite
[1. What city did you choose?]
Orlando, Florida
[2. Which two place types did you select?]
-Restaurants (“restaurant”)
-Food Courts (“food_court”)
[3. How many rows does your dataset contain?]
-The exact number depends on how many restaurants and cafes were returned by the API.
-Google Places limits each response to 20 results per request for a nearby search.
[4. Upon visual inspection, do you notice any spatial patterns in how POIs are distributed across the city?]
-Clusters in downtown Orlando: Restaurants and cafes are denser near the city center and tourist areas.
-Sparse areas: Suburban or residential block groups show fewer POIs.
-Along major roads or highways: Some linear patterns appear where restaurants and cafes follow arterial roads.
[5. Did you observe any other interesting findings?]
-Many POIs are highly rated in downtown, while suburban POIs tend to have fewer user ratings.
-Certain block groups hit the API’s 20-POI limit, suggesting more restaurants exist there than captured — smaller spatial units could give more granular results.
-Food courts are less dense than restaurants but tend to be clustered near commercial or mixed-use zones.