Tidying POI data - Orlando, Florida

Install Libraries

library(tidycensus)
library(sf)
library(tmap)
library(jsonlite)
library(tidyverse)
library(httr)
library(jsonlite)
library(reshape2)
library(here)
library(knitr)

Loading Data

poi <- readRDS("google_poi_data2.rds")

Preview the POI data

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

Remove Duplicates

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.

Flatten list-columns

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 = ", "))
  )

Check if the delimiters are working as expected

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]

Identify which columns are list columns in the dataset

for (col in colnames(poi_unique)){
  if (class(poi_unique[[col]]) == "list"){
    print(col)
  }
}
## [1] "places.types"

Drop rows with missing latitude or longitude

poi_dropna <- poi_flat %>%
  filter(!is.na(places.location.latitude) & !is.na(places.location.longitude))

City boundary

orlando <- tigris::places("FL", progress_bar = FALSE) %>% 
  filter(NAME == 'Orlando') %>% 
  st_transform(4326)
## Retrieving data for the year 2024

Converting poi_dropna into a sf object

poi_sf <- poi_dropna %>% 
  st_as_sf(coords=c("places.location.longitude", "places.location.latitude"), 
           crs = 4326)

POIs within the city boundary

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

Visualize

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

Create review count binary variable

poi_flat <- poi_flat %>%
  mutate(review_count_binary = case_when(
    places.userRatingCount > 500 ~ "many",
    places.userRatingCount <= 500 ~ "few",
    is.na(places.userRatingCount) ~ NA_character_
  ))

Select only relevant columns to simplify output

# 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

Cleaned POI data

# 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"))
First 10 POIs within Orlando
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)

Findings from Orlando POI Dataset

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.