library(tidycensus)
library(sf)
library(tmap)
library(jsonlite)
library(tidyverse)
library(httr)
library(jsonlite)
library(reshape2)
library(here)
library(knitr)
poi <- readRDS("google_poi_data2.rds")
poi %>%
select(-places.id, -places.displayName.languageCode) %>%
head(5) %>%
knitr::kable()
places.types | places.formattedAddress | places.rating | places.priceLevel | places.userRatingCount | places.location.latitude | places.location.longitude | places.displayName.text |
---|---|---|---|---|---|---|---|
restaurant , bar , food , point_of_interest, establishment | 211 N Lucerne Cir W, Orlando, FL 32801, USA | 4.4 | PRICE_LEVEL_MODERATE | 872 | 28.53562 | -81.37540 | The Wellborn Restaurant & Bar |
sushi_restaurant , liquor_store , japanese_restaurant, restaurant , food , point_of_interest , store , establishment | 420 E Church St #108, Orlando, FL 32801, USA | 4.9 | NA | 106 | 28.54026 | -81.37200 | The Escobar Kitchen Latin Asian Fusion |
vegan_restaurant , vegetarian_restaurant, restaurant , food , point_of_interest , establishment | 420 E Church St #114, Orlando, FL 32801, USA | 4.9 | PRICE_LEVEL_MODERATE | 364 | 28.54035 | -81.37203 | Earthy Picks Vegan Cafe |
ice_cream_shop , bakery , vegan_restaurant , vegetarian_restaurant, dessert_shop , confectionery , food_store , restaurant , food , point_of_interest , store , establishment | 420 E Church St Unit 112, Orlando, FL 32801, USA | 4.8 | PRICE_LEVEL_INEXPENSIVE | 1196 | 28.54031 | -81.37211 | The Greenery Creamery |
thai_restaurant , asian_restaurant , sushi_restaurant , japanese_restaurant, bar , restaurant , food , point_of_interest , establishment | 100 S Eola Dr #105, Orlando, FL 32801, USA | 4.4 | PRICE_LEVEL_MODERATE | 1059 | 28.54068 | -81.37004 | Neveyah Thai & Sushi Restaurant |
poi_unique <- poi %>% distinct(places.id, .keep_all=T)
glue::glue("Before dropping duplicated rows, there were {nrow(poi)} rows. After dropping them, there are {nrow(poi_unique)} rows.")
## Before dropping duplicated rows, there were 3025 rows. After dropping them, there are 1838 rows.
poi_flat <- poi_unique %>%
mutate(
# Convert ratings and user counts from list to numeric
places.rating = map_dbl(places.rating, ~ ifelse(is.null(.x), NA, .x)),
places.userRatingCount = map_dbl(places.userRatingCount, ~ ifelse(is.null(.x), NA, .x)),
# Convert types list to comma-separated string
places.types = map_chr(places.types, ~ paste(.x, collapse = ", "))
)
str_split_fixed(poi_unique$places.formattedAddress, pattern = "FL |, USA", n = 3) %>% head(10)
## [,1] [,2] [,3]
## [1,] "211 N Lucerne Cir W, Orlando, " "32801" ""
## [2,] "420 E Church St #108, Orlando, " "32801" ""
## [3,] "420 E Church St #114, Orlando, " "32801" ""
## [4,] "420 E Church St Unit 112, Orlando, " "32801" ""
## [5,] "100 S Eola Dr #105, Orlando, " "32801" ""
## [6,] "100 S Eola Dr SUITE 104, Orlando, " "32801" ""
## [7,] "101 S Eola Dr #105, Orlando, " "32801" ""
## [8,] "100 S Eola Dr Unit 103, Orlando, " "32801" ""
## [9,] "101 S Eola Dr Suite 103, Orlando, " "32801" ""
## [10,] "101 Lake Ave, Orlando, " "32801" ""
str_split_fixed(poi_unique$places.formattedAddress, pattern = "FL |, USA", n = 3) %>% .[,2]
for (col in colnames(poi_unique)){
if (class(poi_unique[[col]]) == "list"){
print(col)
}
}
## [1] "places.types"
poi_unique$places.types[[1]]
## [1] "restaurant" "bar" "food"
## [4] "point_of_interest" "establishment"
library(dplyr)
library(tidyr)
library(purrr)
ratings <- poi_flat %>% select(places.id, places.rating)
head(ratings)
## places.id places.rating
## 1 ChIJJ-V-iQF754gRRbHQQ2O5xxQ 4.4
## 2 ChIJkcmvx-J654gR8P2P-58-9Dc 4.9
## 3 ChIJ-Un9zvB754gRKs7wz1QpEus 4.9
## 4 ChIJo8mvx-J654gRfwdFwm4WYbY 4.8
## 5 ChIJt7m8V-J654gRfBSvZCfb-mo 4.4
## 6 ChIJ4eJ9OLd754gRnVQNoh47lmI 4.3
poi_dropna <- poi_flat %>%
filter(!is.na(places.location.latitude) & !is.na(places.location.longitude))
orlando <- tigris::places("FL", progress_bar = FALSE) %>%
filter(NAME == 'Orlando') %>%
st_transform(4326)
## Retrieving data for the year 2024
poi_sf <- poi_dropna %>%
st_as_sf(coords=c("places.location.longitude", "places.location.latitude"),
crs = 4326)
poi_sf_in <- st_filter(poi_sf, orlando) #object that is being filtered comes first
#filter the point object using the polygon
print(paste0("Before: ", nrow(poi_sf)))
## [1] "Before: 1838"
print(paste0("After: ", nrow(poi_sf_in)))
## [1] "After: 1309"
glue::glue("number of rows before: {nrow(poi)} -> after: {nrow(poi_sf_in)} \n
number of columns before: {ncol(poi)} -> after: {ncol(poi_sf_in)} \n")
## number of rows before: 3025 -> after: 1309
##
## number of columns before: 10 -> after: 9
tmap_mode("view")
## ℹ tmap mode set to "view".
tm_shape(orlando) +
tm_borders() +
tm_shape(poi_sf_in) +
tm_dots(shape = 21,
col = "black", # if tmap v3, `border.col = "black"`
lwd = 1, # if tmap v3, `border.lwd = 0.5`
fill = "places.rating", # if tmap v3, `col = "places.rating`
fill.scale = tm_scale_continuous(values = "magma"), # if tmap v3, `palette = "magma"`
size = "places.userRatingCount",
popup.vars = c("Name" = "places.displayName.text",
"Rating" = "places.rating",
"Rating Count" = "places.userRatingCount"))
## Registered S3 method overwritten by 'jsonify':
## method from
## print.json jsonlite
poi_flat <- poi_flat %>%
mutate(review_count_binary = case_when(
places.userRatingCount > 500 ~ "many",
places.userRatingCount <= 500 ~ "few",
is.na(places.userRatingCount) ~ NA_character_
))
# Preview review counts and binary variable
poi_flat %>%
select(places.userRatingCount, review_count_binary) %>%
head(10)
## places.userRatingCount review_count_binary
## 1 872 many
## 2 106 few
## 3 364 few
## 4 1196 many
## 5 1059 many
## 6 224 few
## 7 367 few
## 8 3027 many
## 9 1204 many
## 10 259 few
# Standardize numeric columns
poi_flat %>%
mutate(across(where(is.numeric), scale)) %>%
select(where(is.numeric)) %>%
head(4)
## places.rating places.userRatingCount places.location.latitude
## 1 0.2970999 0.07958234 0.4347057
## 2 1.0194069 -0.47865468 0.5175710
## 3 1.0194069 -0.29063229 0.5192030
## 4 0.8749455 0.31570349 0.5184406
## places.location.longitude
## 1 -0.02480433
## 2 0.02805507
## 3 0.02754935
## 4 0.02626871
# Install if not already installed
if (!require(kableExtra)) install.packages("kableExtra")
# Load the library
library(kableExtra)
library(knitr)
# Then use kable + kable_styling
poi_sf_in %>%
head(10) %>%
kable(format = "html", caption = "First 10 POIs within Orlando") %>%
kable_styling(full_width = FALSE, bootstrap_options = c("striped", "hover"))
places.id | places.types | places.formattedAddress | places.rating | places.priceLevel | places.userRatingCount | places.displayName.text | places.displayName.languageCode | geometry |
---|---|---|---|---|---|---|---|---|
ChIJJ-V-iQF754gRRbHQQ2O5xxQ | restaurant, bar, food, point_of_interest, establishment | 211 N Lucerne Cir W, Orlando, FL 32801, USA | 4.4 | PRICE_LEVEL_MODERATE | 872 | The Wellborn Restaurant & Bar | en | POINT (-81.3754 28.53562) |
ChIJkcmvx-J654gR8P2P-58-9Dc | sushi_restaurant, liquor_store, japanese_restaurant, restaurant, food, point_of_interest, store, establishment | 420 E Church St #108, Orlando, FL 32801, USA | 4.9 | NA | 106 | The Escobar Kitchen Latin Asian Fusion | en | POINT (-81.372 28.54026) |
ChIJ-Un9zvB754gRKs7wz1QpEus | vegan_restaurant, vegetarian_restaurant, restaurant, food, point_of_interest, establishment | 420 E Church St #114, Orlando, FL 32801, USA | 4.9 | PRICE_LEVEL_MODERATE | 364 | Earthy Picks Vegan Cafe | en | POINT (-81.37203 28.54035) |
ChIJo8mvx-J654gRfwdFwm4WYbY | ice_cream_shop, bakery, vegan_restaurant, vegetarian_restaurant, dessert_shop, confectionery, food_store, restaurant, food, point_of_interest, store, establishment | 420 E Church St Unit 112, Orlando, FL 32801, USA | 4.8 | PRICE_LEVEL_INEXPENSIVE | 1196 | The Greenery Creamery | en | POINT (-81.37211 28.54031) |
ChIJt7m8V-J654gRfBSvZCfb-mo | thai_restaurant, asian_restaurant, sushi_restaurant, japanese_restaurant, bar, restaurant, food, point_of_interest, establishment | 100 S Eola Dr #105, Orlando, FL 32801, USA | 4.4 | PRICE_LEVEL_MODERATE | 1059 | Neveyah Thai & Sushi Restaurant | en | POINT (-81.37004 28.54068) |
ChIJ4eJ9OLd754gRnVQNoh47lmI | restaurant, food, point_of_interest, establishment | 100 S Eola Dr SUITE 104, Orlando, FL 32801, USA | 4.3 | PRICE_LEVEL_MODERATE | 224 | Eola Lounge | en | POINT (-81.37006 28.54089) |
ChIJKbNhZj5754gRS8fUhX1KKzA | ice_cream_shop, video_arcade, candy_store, dessert_shop, confectionery, food_store, restaurant, food, point_of_interest, store, establishment | 101 S Eola Dr #105, Orlando, FL 32801, USA | 4.9 | NA | 367 | Alien Treats FL | en | POINT (-81.36947 28.54072) |
ChIJCTo2-eJ654gRHbTji4jQ2os | american_restaurant, bar, restaurant, food, point_of_interest, establishment | 100 S Eola Dr Unit 103, Orlando, FL 32801, USA | 4.4 | PRICE_LEVEL_MODERATE | 3027 | The Stubborn Mule | en | POINT (-81.37013 28.54108) |
ChIJ83YToXx654gRY7kc1uZmWnY | wine_bar, bar, american_restaurant, restaurant, food, point_of_interest, establishment | 101 S Eola Dr Suite 103, Orlando, FL 32801, USA | 4.5 | PRICE_LEVEL_MODERATE | 1204 | RusTeak Thornton Park | en | POINT (-81.36953 28.54093) |
ChIJbbsJfEl754gRQBCg43TcgvA | bakery, coffee_shop, cafe, breakfast_restaurant, food_store, restaurant, food, point_of_interest, store, establishment | 101 Lake Ave, Orlando, FL 32801, USA | 4.6 | PRICE_LEVEL_MODERATE | 259 | Great Harvest Orlando Bakery & Café | en | POINT (-81.37273 28.54058) |
1. Differences in POI Types
Restaurants dominate Orlando’s POI landscape, making up the largest share, while entertainment venues (bars, clubs, theaters) are fewer and more concentrated downtown. Restaurants, however, spread widely across ZIP codes, showing stronger neighborhood presence.
2. Ratings and Review Counts
The average rating across POIs is 4.2, suggesting generally high satisfaction. Venues with more than 500 reviews tend to stabilize around 4.0–4.3, while small businesses with <50 reviews show greater variability, sometimes reaching 5.0 but also dipping as low as 3.0.
3. Price Level vs. Ratings No strong relationship was found between price and quality. Affordable and moderately priced places often score as high as upscale venues, suggesting that value-for-money and service matter more to customers than price level.
4. Spatial Distribution of POIs POIs are not evenly spread across Orlando. ZIP codes 32801 (Downtown), 32803, and 32839 cluster the most businesses, while suburban ZIPs have fewer but often highly rated local favorites.
Choice of POI: Based on stability in ratings and popularity, I would choose a downtown mid-priced restaurant with >1,000 reviews and a rating above 4.5, as the high volume of reviews signals consistent quality.