Data Loading and Initial Exploration

# Load the datasets

df_business <- read_csv('business_AZ_3.csv')
df_review <- read_csv('review_AZ_3.csv')
df_tip <- read_csv('tip_AZ_3.csv')

cat("Business dataset shape:", dim(df_business)[1], "x", dim(df_business)[2], "\n")
## Business dataset shape: 6187 x 71
cat("Review dataset shape:", dim(df_review)[1], "x", dim(df_review)[2], "\n")
## Review dataset shape: 350340 x 8
cat("Tip dataset shape:", dim(df_tip)[1], "x", dim(df_tip)[2], "\n")
## Tip dataset shape: 54219 x 4
# Check business data structure
cat("\nBusiness data sample:\n")
## 
## Business data sample:
knitr::kable(head(df_business, 10), 
             caption = "Sample of Business Data")
Sample of Business Data
business_id name address city state postal_code latitude longitude stars review_count is_open attributes.ByAppointmentOnly attributes.BusinessAcceptsCreditCards attributes.BikeParking attributes.RestaurantsPriceRange2 attributes.CoatCheck attributes.RestaurantsTakeOut attributes.RestaurantsDelivery attributes.Caters attributes.WiFi attributes.BusinessParking attributes.WheelchairAccessible attributes.HappyHour attributes.OutdoorSeating attributes.HasTV attributes.RestaurantsReservations attributes.DogsAllowed attributes.Alcohol attributes.GoodForKids attributes.RestaurantsAttire attributes.Ambience attributes.RestaurantsTableService attributes.RestaurantsGoodForGroups attributes.DriveThru attributes.NoiseLevel attributes.GoodForMeal attributes.BusinessAcceptsBitcoin attributes.Smoking attributes.Music attributes.GoodForDancing attributes.AcceptsInsurance attributes.BestNights attributes.BYOB attributes.Corkage attributes.BYOBCorkage attributes.HairSpecializesIn attributes.Open24Hours attributes.RestaurantsCounterService attributes.AgesAllowed attributes.DietaryRestrictions categories hours.Monday hours.Tuesday hours.Wednesday hours.Thursday hours.Friday hours.Saturday hours.Sunday Food_Beverage Health_Fitness Personal_Care Automotive Retail Professional_Services Entertainment Travel_Hospitality Education Home_Services Healthcare Veterinary_Services Sum_of_Industries
tUFrWirKiKi_TAnsVWINQQ Target 5255 E Broadway Blvd Tucson AZ 85711 32.22324 -110.8805 3.5 22 0 False True TRUE 2 FALSE False False FALSE u’no’ {‘garage’: False ‘street’: False ‘validated’: False ‘lot’: True ‘valet’: False} TRUE FALSE False FALSE False False NA NA NA NA NA NA NA NA NA NA NA NA NA NA NA NA NA NA NA NA NA NA NA Department Stores, Shopping, Fashion, Home & Garden, Electronics, Furniture Stores 8:0-22:0 8:0-22:0 8:0-22:0 8:0-22:0 8:0-23:0 8:0-23:0 8:0-22:0 0 0 0 0 1 0 0 0 0,0,0,0,1
wghnIlMb_i5U46HMBGx9ig China Dragon Restaurant 1625 W Valencia Rd, Ste 101-103 Tucson AZ 85746 32.13230 -111.0000 3.0 23 0 NA True NA 2 NA True False FALSE ‘no’ {‘garage’: False ‘street’: False ‘validated’: False ‘lot’: False ‘valet’: False} NA NA False FALSE True NA ‘beer_and_wine’ True u’casual’ {‘romantic’: False ‘intimate’: False ‘classy’: False ‘hipster’: False ‘divey’: False ‘touristy’: False ‘trendy’: False ‘upscale’: False ‘casual’: True} NA True NA u’quiet’ NA NA NA NA NA NA NA NA NA NA NA NA NA NA NA Restaurants, Chinese 11:0-21:0 11:0-21:0 11:0-21:0 11:0-21:0 11:0-21:0 11:0-21:0 11:0-21:0 1,0,0,0,0,0,0,0,0,0,0,0,1
txyXRytGjwOXvS8s4sc-WA Smoothie King 1070 E Tucson Marketplace Blvd Tucson AZ 85713 32.18679 -110.9548 3.0 29 1 NA True TRUE 2 NA True True NA u’free’ {u’valet’: False u’garage’: False u’street’: None u’lot’: True u’validated’: False} FALSE NA NA NA NA NA NA NA NA NA NA NA NA NA NA NA NA NA NA NA NA NA NA NA NA NA NA NA NA Vitamins & Supplements, Ice Cream & Frozen Yogurt, Food, Juice Bars & Smoothies, Shopping 0:0-0:0 7:0-21:0 7:0-19:0 7:0-19:0 7:0-19:0 9:0-20:0 11:0-18:0 1 0 0 0 1 0 0 0 0,0,0,0,2
eYxGFkxo6m3SYGVTh5m2nQ Big Boyz Toyz Motorcycle Rentals 4158 E Grant Rd Tucson AZ 85712 32.25032 -110.9037 4.5 8 1 NA NA NA NA NA NA NA NA NA NA NA NA NA NA NA NA NA NA NA NA NA NA NA NA NA NA NA NA NA NA NA NA NA NA NA NA NA NA NA Towing, Hotels & Travel, Automotive, Motorcycle Rental, Motorcycle Gear, Shopping 8:30-18:0 8:30-18:0 8:30-18:0 8:30-18:0 8:30-18:0 9:0-16:0 NA 0 0 0 1 1 0 0 1 0 0 0 0 3
x9K0RfZaT_zlw6DklBDzjw Gavi Italian Restaurant 7401 N La Cholla Blvd, Ste 146 Tucson AZ 85707 32.22167 -110.9258 3.5 9 0 NA True NA 2 NA True False NA NA {‘garage’: False ‘street’: False ‘validated’: False ‘lot’: True ‘valet’: False} NA NA True NA False NA u’full_bar’ True u’casual’ NA NA True NA NA NA NA NA NA NA NA NA NA NA NA NA NA NA NA NA Italian, Restaurants NA NA NA NA NA NA NA 1 0 0 0 0 0 0 0 0,0,0,0,1
luGY5CJlb4CJ3LrBcSXMPA Desert Design Center 4146 E Speedway Blvd Tucson AZ 85712 32.23587 -110.9037 3.0 5 1 NA True NA 3 NA NA NA NA NA {‘garage’: False ‘street’: False ‘validated’: False ‘lot’: False ‘valet’: False} NA NA NA NA NA NA NA NA NA NA NA NA NA NA NA NA NA NA NA NA NA NA NA NA NA NA NA NA NA Furniture Stores, Shopping, Home & Garden 9:30-18:0 9:30-18:0 9:30-18:0 9:30-18:0 9:30-18:0 9:30-18:0 12:0-17:0 0 0 0 0 1 0 0 0 0,0,0,0,1
wEfK2HIYNzkA1QYfUjHBaw Leslie’s 7955 E Broadway Blvd, Ste 147 Tucson AZ 85710 32.22154 -110.8221 2.5 15 1 NA True TRUE 2 NA NA NA NA NA None NA NA NA NA NA NA NA NA NA NA NA NA NA NA NA NA NA NA NA NA NA NA NA NA NA NA NA NA NA Hardware Stores, Home Services, Hot Tub & Pool, Pool & Hot Tub Service, Home & Garden, Shopping 9:0-17:0 7:0-18:0 7:0-18:0 7:0-18:0 7:0-18:0 9:0-18:0 10:0-17:0 0 0 0 0 1 0 0 0 0 0 0 0 1
anLQj9AM8vjbcLSIE0iUgg Papa Murphy’s 7250 North La Cholla, Suite 186 Tucson AZ 85741 32.33857 -111.0108 4.0 22 1 NA True FALSE 1 NA True True NA NA {‘garage’: False ‘street’: False ‘validated’: False ‘lot’: False ‘valet’: False} NA NA False TRUE False NA u’none’ True u’casual’ NA NA True True u’quiet’ NA NA NA NA NA NA NA NA NA NA NA NA NA NA NA Pizza, Restaurants NA NA NA NA NA NA NA 1 0 0 0 0 0 0 0 0,0,0,0,1
9C2rpb56aQvW0ViZHK9sPw Home Plate Sports Pub 4880 E 22nd St Tucson AZ 85711 32.20631 -110.8878 3.5 56 1 NA True TRUE 1 NA NA False FALSE u’no’ {u’valet’: False u’garage’: False u’street’: None u’lot’: None u’validated’: None} NA TRUE True TRUE False NA u’full_bar’ True u’casual’ {u’divey’: None u’hipster’: None u’casual’: True u’touristy’: None u’trendy’: None u’intimate’: False u’romantic’: None u’classy’: False u’upscale’: None} NA True NA u’average’ NA NA NA {u’dj’: None u’live’: False u’jukebox’: None u’video’: False u’background_music’: False u’karaoke’: None u’no_music’: False} NA NA {u’monday’: True u’tuesday’: False u’wednesday’: False u’thursday’: False u’friday’: False u’saturday’: False u’sunday’: False} NA NA NA NA NA,NA,NA,NA,“Bars, Restaurants, Sports Bars, Nightlife”,10:0-2:0,10:0-2:0,10:0-2:0,10:0-2:0,10:0-2:0,10:0-2:0,10:0-2:0,1,0,0,0,0,0,0,0,0,0,0,0,1
E_kLUlU60fzoZYAOxbH_3w Pain Institute of Southern Arizona 4582 N 1st Ave, Ste 170 Tucson AZ 85718 32.28918 -110.9572 2.5 43 1 True True NA NA NA NA NA NA NA NA NA NA NA NA NA NA NA NA NA NA NA NA NA NA NA NA NA NA NA True NA NA NA NA NA NA NA NA NA Pain Management, Doctors, Health & Medical, Osteopathic Physicians 7:0-16:30 7:0-16:30 7:0-16:30 7:0-16:30 7:0-16:30 NA NA 0 0 0 0 0 0 0 0 0 0 1 0 1
# Check review data structure  
cat("\nReview data sample:\n")
## 
## Review data sample:
knitr::kable(head(df_review, 10), 
             caption = "Sample of Review Data")
Sample of Review Data
review_id user_id business_id stars useful funny cool date
saUsX_uimxRlCVr67Z4Jig 8g_iMtfSiwikVnbP2etR0A YjUWPpI6HXG530lwP-fb2A 3 0 0 0 2014-02-05 20:30:30
UBp0zWyH60Hmw6Fsasei7w 4Uh27DgGzsp6PqrH913giQ otQS34_MymijPTdNBoBdCw 4 0 2 0 2011-10-27 17:12:05
ymhbOMW63B_vGaRFR3XT0A yZdAhQ_KmKuCZmbBy9YDQg 5Ce3lZksYVkCbrihqylVHQ 5 0 0 0 2014-07-25 17:56:26
onlgwy5qGDEzddsrnIvtWg pYXeL0RCqus2IfhthYCOyA W7NxQw8UYFR0HLPrI08tvw 4 0 0 0 2012-02-01 14:21:25
ypFqmURIY41F4pWaMW1VrQ hToW2eoTAYlOcuNz7I73Cg 7L1kXfwU5XM6f6Jxo_L7Yg 5 1 0 0 2015-07-03 22:36:04
_yfUDZFGYfc4Lmq2l-U7ZA jVVNbLCq2PaxMYtdNkxWGA VO5Jphyqmoa62lbA7-vzpQ 5 0 0 0 2013-05-07 15:19:23
M7h8K7PrLrN8yQb3RhHLdA CysTz612pTix75Fye6-D4Q WLiqfxv_GhFFA5sm878a2w 5 0 1 1 2013-12-29 03:15:34
dEVDg2FyOR2sJnbR_JcivQ 7zJVt_Bl1h5hLjhxl2c1PQ 3YERGr7UbpSpddqL0Eiu5g 5 1 0 1 2017-12-16 00:18:10
_u331rYIp7qJ7aWvLup9Ug BUpAhzsbbKbTqyBjT8YnNQ wa_bwyY57etHjtJ2Fw0E3g 5 0 0 0 2014-04-01 05:00:09
RgonT2qGpUte6RFpoSSxuQ iaLZ0oJ3sqCpnW-PY5_i4w dygflr6j51vf3-lM5pUIkw 3 0 0 0 2017-04-07 21:52:05
# Check tip data structure  
cat("\nTip data sample:\n")
## 
## Tip data sample:
knitr::kable(head(df_tip, 10), 
             caption = "Sample of Tip Data")
Sample of Tip Data
user_id business_id date compliment_count
AGNUgVwnZUey3gcPCJ76iw 3uLgwr0qeCNMjKenHJwPGQ 2012-05-18 02:17:21 0
1viszljzsa8W7VnYPuWosw Zg83olSjsdXAhE5EEy5IcQ 2018-04-01 01:40:12 0
evVjF1TOaNwuUK7oY2k53w 5yjHCLzwHgjKNNHgAaUC8Q 2012-03-28 23:27:41 0
ztPinMY7A5o4HteSMyBzdw 1lSj24GO6VMJ2uB6hW3pGA 2017-03-17 21:35:00 0
juigu8XxwzLAeikeBzULbg DlOZoZZRLEOuh9FdAw1few 2013-08-18 23:54:19 0
8ohYIH9wHjKdWc4yAROqBw 3w7NRntdQ9h0KwDsksIt5Q 2013-03-09 19:23:16 0
td7JrqJMybMZKKoa1h9Qsg tV46IhCfHbsx_af-pMupiw 2012-05-30 20:19:18 0
wzX8UssOglKwSDx8qob8zA yZwlpIhzVRxVdbIkuw9-VA 2015-09-10 05:31:03 0
4Dy4PVb5lykv4N0kYiRhVA HpmsY3JJ4gIuS8EiVHwMJw 2017-11-18 21:11:26 0
3vofzamIMkrS0PPf5aSmRQ H5nMeiXJmLpBALeV8A-FeQ 2011-08-04 04:24:32 0

Feature Engineering from Review Data

# Create aggregated review features by business_id

review_features <- df_review |> 
  group_by(business_id) |>
  summarise(
    avg_review_stars = mean(stars, na.rm = TRUE),
    total_reviews = n(),
    avg_useful = mean(useful, na.rm = TRUE),
    avg_funny = mean(funny, na.rm = TRUE),
    avg_cool = mean(cool, na.rm = TRUE),
    most_recent_review = max(date, na.rm = TRUE), 
    .groups = 'drop'
  )

cat("Review features created for", nrow(review_features), "businesses\n")
## Review features created for 6187 businesses
knitr::kable(head(review_features, 10), 
             caption = "Aggregated Review Features by Business", 
             digits = 3)
Aggregated Review Features by Business
business_id avg_review_stars total_reviews avg_useful avg_funny avg_cool most_recent_review
–9osgUCSDUWUkoTLdvYhQ 4.800 30 0.800 0.133 0.633 2022-01-07 16:50:23
–MbOh2O1pATkXa7xbU6LA 3.963 27 0.778 0.259 0.259 2017-11-12 01:40:47
–Sd93OFWITqDHifMoAy9g 4.333 6 0.833 0.000 0.000 2019-06-05 22:44:05
-02xFuruu85XmDn2xiynJw 4.686 121 0.744 0.066 0.124 2021-06-24 15:59:50
-0E708CsutslDLUuuzWGYQ 2.208 24 2.000 0.333 0.167 2020-01-16 02:34:38
-0G_6-KFGpCpxTUlVXCMYQ 3.578 154 2.429 0.474 1.234 2015-03-07 23:46:33
-0yr__Vo4hOCZwvzRWd1zA 4.111 9 1.444 0.111 0.333 2019-07-13 21:11:15
-1MhPXk1FglglUAmuPLIGg 3.877 114 0.825 0.272 0.544 2021-11-30 15:15:15
-1iLbEf1NwY-OJp5Hg-3Sg 3.154 13 0.923 0.154 0.308 2019-10-23 01:35:34
-1w9JMktu9oWTXwNqtZQoA 3.882 17 0.824 0.118 0.412 2021-07-06 14:02:22

Merge Business and Review Data

# Merge business data with review features
df_merged <- df_business |> 
  left_join(review_features, by = "business_id") |>
  filter(!is.na(avg_review_stars)) # remove businesses without reviews

cat("Merged dataset shape:", dim(df_merged)[1], "x", dim(df_merged)[2], "\n")
## Merged dataset shape: 6187 x 77
# Check what columns we actually have
cat("\nColumns in merged dataset:\n")
## 
## Columns in merged dataset:
print(colnames(df_merged))
##  [1] "business_id"                          
##  [2] "name"                                 
##  [3] "address"                              
##  [4] "city"                                 
##  [5] "state"                                
##  [6] "postal_code"                          
##  [7] "latitude"                             
##  [8] "longitude"                            
##  [9] "stars"                                
## [10] "review_count"                         
## [11] "is_open"                              
## [12] "attributes.ByAppointmentOnly"         
## [13] "attributes.BusinessAcceptsCreditCards"
## [14] "attributes.BikeParking"               
## [15] "attributes.RestaurantsPriceRange2"    
## [16] "attributes.CoatCheck"                 
## [17] "attributes.RestaurantsTakeOut"        
## [18] "attributes.RestaurantsDelivery"       
## [19] "attributes.Caters"                    
## [20] "attributes.WiFi"                      
## [21] "attributes.BusinessParking"           
## [22] "attributes.WheelchairAccessible"      
## [23] "attributes.HappyHour"                 
## [24] "attributes.OutdoorSeating"            
## [25] "attributes.HasTV"                     
## [26] "attributes.RestaurantsReservations"   
## [27] "attributes.DogsAllowed"               
## [28] "attributes.Alcohol"                   
## [29] "attributes.GoodForKids"               
## [30] "attributes.RestaurantsAttire"         
## [31] "attributes.Ambience"                  
## [32] "attributes.RestaurantsTableService"   
## [33] "attributes.RestaurantsGoodForGroups"  
## [34] "attributes.DriveThru"                 
## [35] "attributes.NoiseLevel"                
## [36] "attributes.GoodForMeal"               
## [37] "attributes.BusinessAcceptsBitcoin"    
## [38] "attributes.Smoking"                   
## [39] "attributes.Music"                     
## [40] "attributes.GoodForDancing"            
## [41] "attributes.AcceptsInsurance"          
## [42] "attributes.BestNights"                
## [43] "attributes.BYOB"                      
## [44] "attributes.Corkage"                   
## [45] "attributes.BYOBCorkage"               
## [46] "attributes.HairSpecializesIn"         
## [47] "attributes.Open24Hours"               
## [48] "attributes.RestaurantsCounterService" 
## [49] "attributes.AgesAllowed"               
## [50] "attributes.DietaryRestrictions"       
## [51] "categories"                           
## [52] "hours.Monday"                         
## [53] "hours.Tuesday"                        
## [54] "hours.Wednesday"                      
## [55] "hours.Thursday"                       
## [56] "hours.Friday"                         
## [57] "hours.Saturday"                       
## [58] "hours.Sunday"                         
## [59] "Food_Beverage"                        
## [60] "Health_Fitness"                       
## [61] "Personal_Care"                        
## [62] "Automotive"                           
## [63] "Retail"                               
## [64] "Professional_Services"                
## [65] "Entertainment"                        
## [66] "Travel_Hospitality"                   
## [67] "Education"                            
## [68] "Home_Services"                        
## [69] "Healthcare"                           
## [70] "Veterinary_Services"                  
## [71] "Sum_of_Industries"                    
## [72] "avg_review_stars"                     
## [73] "total_reviews"                        
## [74] "avg_useful"                           
## [75] "avg_funny"                            
## [76] "avg_cool"                             
## [77] "most_recent_review"
# Check for missing values in key columns that exist
key_cols <- c("stars", "avg_review_stars", "total_reviews", "avg_useful", "avg_funny", "avg_cool")
existing_cols <- key_cols[key_cols %in% colnames(df_merged)]

if(length(existing_cols) > 0) {
  cat("\nMissing values in key columns:\n")
  
missing_summary <- df_merged[existing_cols] |>
  summarise_all(~sum(is.na(.)))
  
  knitr::kable(missing_summary, 
               caption = "Missing Values Summary", 
               digits = 0)
} else {
  cat("Key columns not found in dataset. Please check column names.\n")
}
## 
## Missing values in key columns:
Missing Values Summary
stars avg_review_stars total_reviews avg_useful avg_funny avg_cool
0 0 0 0 0 0

Target Variable Creation

# Create binary classification target (High vs. Low) rating
df_merged <- df_merged |>
  mutate(rating_category = case_when(    
    avg_review_stars < 3.0 ~ "Low",
    avg_review_stars >= 3.0 & stars < 4.0 ~ "Medium",
    avg_review_stars >= 4.0 ~ "High"  
),  
# Remove Medium category for binary classification   
binary_target = case_when (
  rating_category == "Low" ~ 0,
  rating_category == "High" ~ 1
  )
) |>
  filter(rating_category %in% c("Low", "High")) # Keep only Low and High

cat("Target variable distribution:\n")
## Target variable distribution:
target_dist <- table(df_merged$rating_category)
knitr::kable(as.data.frame(target_dist), 
             caption = "Rating Category Distribution",
             col.names = c("Rating Category", "Count"))
Rating Category Distribution
Rating Category Count
High 2337
Low 1547
cat("\nBinary target distribution:\n")
## 
## Binary target distribution:
binary_dist <- table(df_merged$binary_target)
knitr::kable(as.data.frame(binary_dist), 
             caption = "Binary Target Distribution",
             col.names = c("Binary Target (0=Low, 1=High)", "Count"))
Binary Target Distribution
Binary Target (0=Low, 1=High) Count
0 1547
1 2337
# Regression target is the numeric 'stars' column
cat("\nRegression target (stars) summary:\n")
## 
## Regression target (stars) summary:
stars_summary <- df_merged |> 
  summarise(
    Min = min(stars, na.rm = TRUE),
    Q1 = quantile(stars, 0.25, na.rm = TRUE),
    Median = median(stars, na.rm = TRUE),
    Mean = mean(stars, na.rm = TRUE),
    Q3 = quantile(stars, 0.75, na.rm = TRUE),
    Max = max(stars, na.rm = TRUE),
    SD = sd(stars, na.rm = TRUE)
  )

knitr::kable(stars_summary, 
             caption = "Summary Statistics for Business Star Ratings",
             digits = 3)
Summary Statistics for Business Star Ratings
Min Q1 Median Mean Q3 Max SD
1 2.5 4 3.63 4.5 5 1.117

Feature Selection and Preparation

# First, let's check what columns are actually in our merged dataset
cat("Columns in df_merged:\n")
## Columns in df_merged:
print(colnames(df_merged))
##  [1] "business_id"                          
##  [2] "name"                                 
##  [3] "address"                              
##  [4] "city"                                 
##  [5] "state"                                
##  [6] "postal_code"                          
##  [7] "latitude"                             
##  [8] "longitude"                            
##  [9] "stars"                                
## [10] "review_count"                         
## [11] "is_open"                              
## [12] "attributes.ByAppointmentOnly"         
## [13] "attributes.BusinessAcceptsCreditCards"
## [14] "attributes.BikeParking"               
## [15] "attributes.RestaurantsPriceRange2"    
## [16] "attributes.CoatCheck"                 
## [17] "attributes.RestaurantsTakeOut"        
## [18] "attributes.RestaurantsDelivery"       
## [19] "attributes.Caters"                    
## [20] "attributes.WiFi"                      
## [21] "attributes.BusinessParking"           
## [22] "attributes.WheelchairAccessible"      
## [23] "attributes.HappyHour"                 
## [24] "attributes.OutdoorSeating"            
## [25] "attributes.HasTV"                     
## [26] "attributes.RestaurantsReservations"   
## [27] "attributes.DogsAllowed"               
## [28] "attributes.Alcohol"                   
## [29] "attributes.GoodForKids"               
## [30] "attributes.RestaurantsAttire"         
## [31] "attributes.Ambience"                  
## [32] "attributes.RestaurantsTableService"   
## [33] "attributes.RestaurantsGoodForGroups"  
## [34] "attributes.DriveThru"                 
## [35] "attributes.NoiseLevel"                
## [36] "attributes.GoodForMeal"               
## [37] "attributes.BusinessAcceptsBitcoin"    
## [38] "attributes.Smoking"                   
## [39] "attributes.Music"                     
## [40] "attributes.GoodForDancing"            
## [41] "attributes.AcceptsInsurance"          
## [42] "attributes.BestNights"                
## [43] "attributes.BYOB"                      
## [44] "attributes.Corkage"                   
## [45] "attributes.BYOBCorkage"               
## [46] "attributes.HairSpecializesIn"         
## [47] "attributes.Open24Hours"               
## [48] "attributes.RestaurantsCounterService" 
## [49] "attributes.AgesAllowed"               
## [50] "attributes.DietaryRestrictions"       
## [51] "categories"                           
## [52] "hours.Monday"                         
## [53] "hours.Tuesday"                        
## [54] "hours.Wednesday"                      
## [55] "hours.Thursday"                       
## [56] "hours.Friday"                         
## [57] "hours.Saturday"                       
## [58] "hours.Sunday"                         
## [59] "Food_Beverage"                        
## [60] "Health_Fitness"                       
## [61] "Personal_Care"                        
## [62] "Automotive"                           
## [63] "Retail"                               
## [64] "Professional_Services"                
## [65] "Entertainment"                        
## [66] "Travel_Hospitality"                   
## [67] "Education"                            
## [68] "Home_Services"                        
## [69] "Healthcare"                           
## [70] "Veterinary_Services"                  
## [71] "Sum_of_Industries"                    
## [72] "avg_review_stars"                     
## [73] "total_reviews"                        
## [74] "avg_useful"                           
## [75] "avg_funny"                            
## [76] "avg_cool"                             
## [77] "most_recent_review"                   
## [78] "rating_category"                      
## [79] "binary_target"
# Check the structure of key industry columns to see what we're dealing with
cat("\nChecking industry column types:\n")
## 
## Checking industry column types:
industry_cols <- c("Food_Beverage", "Retail", "Healthcare", "Entertainment", "Health_Fitness")
existing_industry_cols <- industry_cols[industry_cols %in% colnames(df_merged)]

if(length(existing_industry_cols) > 0) {
  for(col in existing_industry_cols) {
    cat(paste("Column:", col, "- Class:", class(df_merged[[col]]), "- Sample values:", 
              paste(head(unique(df_merged[[col]]), 3), collapse = ", "), "\n"))
  }
}
## Column: Food_Beverage - Class: character - Sample values: 0, 9:30-18:0, 9:0-17:0 
## Column: Retail - Class: character - Sample values: 1, 0, NA 
## Column: Healthcare - Class: character - Sample values: 0, 1, NA 
## Column: Entertainment - Class: character - Sample values: 0, NA, 1 
## Column: Health_Fitness - Class: character - Sample values: 0, 9:30-18:0, 10:0-15:0
# Select predictor variables that we know exist and are properly formatted
# Start with basic business characteristics that should be numeric
safe_predictor_cols <- c()

# Add review count if it exists
if("review_count" %in% colnames(df_merged)) {
  safe_predictor_cols <- c(safe_predictor_cols, "review_count")
}

# Add location data if it exists  
if("latitude" %in% colnames(df_merged) && "longitude" %in% colnames(df_merged)) {
  safe_predictor_cols <- c(safe_predictor_cols, "latitude", "longitude")
}

# Add aggregated review features (but exclude avg_review_stars to avoid data leakage)
review_feature_cols <- c("total_reviews", "avg_useful", "avg_funny", "avg_cool")
existing_review_cols <- review_feature_cols[review_feature_cols %in% colnames(df_merged)]
safe_predictor_cols <- c(safe_predictor_cols, existing_review_cols)

# Only add industry columns if they're properly formatted (numeric 0/1)
for(col in existing_industry_cols) {
  if(is.numeric(df_merged[[col]]) && all(df_merged[[col]] %in% c(0, 1, NA))) {
    safe_predictor_cols <- c(safe_predictor_cols, col)
    cat("Added industry column:", col, "\n")
  } else {
    cat("Skipping problematic industry column:", col, "\n")
  }
}
## Skipping problematic industry column: Food_Beverage 
## Skipping problematic industry column: Retail 
## Skipping problematic industry column: Healthcare 
## Skipping problematic industry column: Entertainment 
## Skipping problematic industry column: Health_Fitness
cat("\nUsing predictor columns:", paste(safe_predictor_cols, collapse = ", "), "\n")
## 
## Using predictor columns: review_count, latitude, longitude, total_reviews, avg_useful, avg_funny, avg_cool
# Create feature matrix with only safe, properly formatted columns
X <- df_merged[safe_predictor_cols] |>
  mutate_all(~as.numeric(as.character(.))) |>  # Ensure all are numeric  
  mutate_all(~ifelse(is.na(.), 0, .))  # Replace NAs with 0# Target variables
y_binary <- df_merged$binary_target
y_regression <- df_merged$avg_review_stars

cat("Feature matrix shape:", dim(X)[1], "x", dim(X)[2], "\n")
## Feature matrix shape: 3884 x 7
cat("Features used:\n")
## Features used:
# Create a nice table showing the features we're actually using
if(ncol(X) > 0) {
  feature_info <- data.frame(
    Feature_Name = colnames(X),
    Feature_Type = ifelse(colnames(X) %in% existing_industry_cols, "Industry Indicator",
                         ifelse(colnames(X) %in% c("review_count", "latitude", "longitude"), "Business Characteristic",
                               "Aggregated Review Metric")),
    Data_Type = sapply(X, class),
    Sample_Range = sapply(X, function(x) paste(round(range(x, na.rm = TRUE), 2), collapse = " to "))
  )
  
  knitr::kable(feature_info, 
               caption = "Predictor Variables Used in Models",
               col.names = c("Feature Name", "Type", "Data Type", "Range"))
} else {
  cat("No valid predictor columns found!\n")
}
Predictor Variables Used in Models
Feature Name Type Data Type Range
review_count review_count Business Characteristic numeric 5 to 2126
latitude latitude Business Characteristic numeric 31.9 to 32.53
longitude longitude Business Characteristic numeric -111.22 to -110.63
total_reviews total_reviews Aggregated Review Metric numeric 5 to 2230
avg_useful avg_useful Aggregated Review Metric numeric 0 to 16.55
avg_funny avg_funny Aggregated Review Metric numeric 0 to 5.27
avg_cool avg_cool Aggregated Review Metric numeric 0 to 5.82

Data Splitting

# Split data for classification
train_idx_cls <- createDataPartition(y_binary, p = 0.8, list = FALSE)
X_train_cls <- X[train_idx_cls, ]
X_test_cls <- X[-train_idx_cls, ]
y_train_cls <- y_binary[train_idx_cls]
y_test_cls <- y_binary[-train_idx_cls]

# Split data for regression  
train_idx_reg <- createDataPartition(y_regression, p = 0.8, list = FALSE)
X_train_reg <- X[train_idx_reg, ]
X_test_reg <- X[-train_idx_reg, ]
y_train_reg <- y_regression[train_idx_reg]
y_test_reg <- y_regression[-train_idx_reg]

cat("Classification - Training set:", dim(X_train_cls)[1], "Test set:", dim(X_test_cls)[1], "\n")
## Classification - Training set: 3108 Test set: 776
cat("Regression - Training set:", dim(X_train_reg)[1], "Test set:", dim(X_test_reg)[1], "\n")
## Regression - Training set: 3110 Test set: 774

Binary Classification Model

cat("=" %>% rep(60) %>% paste(collapse = ""), "\n")
## ============================================================
cat("BINARY CLASSIFICATION: High vs Low Rating\n")
## BINARY CLASSIFICATION: High vs Low Rating
cat("=" %>% rep(60) %>% paste(collapse = ""), "\n")
## ============================================================
# Prepare training data - convert binary target to proper factor levels
train_data_cls <- cbind(X_train_cls, target = factor(y_train_cls, levels = c(0, 1), labels = c("Low", "High")))

# Set up cross-validation
ctrl <- trainControl(method = "cv", number = 5, classProbs = TRUE, summaryFunction = twoClassSummary)

# Train Random Forest model
rf_model_cls <- train(target ~ ., 
                      data = train_data_cls,
                      method = "rf",
                      trControl = ctrl,
                      tuneGrid = expand.grid(mtry = c(2, 3, 4, 5)),
                      ntree = 100,
                      metric = "ROC")

cat("Best parameters:\n")
## Best parameters:
print(rf_model_cls$bestTune)
##   mtry
## 3    4
cat("Best cross-validation ROC:", round(max(rf_model_cls$results$ROC), 4), "\n")
## Best cross-validation ROC: 0.8604
# Predict on test set
y_pred_cls <- predict(rf_model_cls, newdata = X_test_cls)
y_pred_prob <- predict(rf_model_cls, newdata = X_test_cls, type = "prob")[,"High"]

# Convert test labels to same factor format for evaluation
y_test_cls_factor <- factor(y_test_cls, levels = c(0, 1), labels = c("Low", "High"))

# Evaluate model
conf_matrix <- confusionMatrix(y_pred_cls, y_test_cls_factor, positive = "High")
print(conf_matrix)
## Confusion Matrix and Statistics
## 
##           Reference
## Prediction Low High
##       Low  217   90
##       High  83  386
##                                              
##                Accuracy : 0.7771             
##                  95% CI : (0.7461, 0.8059)   
##     No Information Rate : 0.6134             
##     P-Value [Acc > NIR] : <0.0000000000000002
##                                              
##                   Kappa : 0.532              
##                                              
##  Mcnemar's Test P-Value : 0.6483             
##                                              
##             Sensitivity : 0.8109             
##             Specificity : 0.7233             
##          Pos Pred Value : 0.8230             
##          Neg Pred Value : 0.7068             
##              Prevalence : 0.6134             
##          Detection Rate : 0.4974             
##    Detection Prevalence : 0.6044             
##       Balanced Accuracy : 0.7671             
##                                              
##        'Positive' Class : High               
## 
# Calculate additional metrics
precision <- conf_matrix$byClass['Precision']
recall <- conf_matrix$byClass['Recall'] 
f1_score <- conf_matrix$byClass['F1']
accuracy <- conf_matrix$overall['Accuracy']

cat("\nClassification Metrics:\n")
## 
## Classification Metrics:
cat("Accuracy:", round(accuracy, 4), "\n")
## Accuracy: 0.7771
cat("Precision:", round(precision, 4), "\n")
## Precision: 0.823
cat("Recall:", round(recall, 4), "\n")
## Recall: 0.8109
cat("F1 Score:", round(f1_score, 4), "\n")
## F1 Score: 0.8169

Regression Model

cat("=" %>% rep(60) %>% paste(collapse = ""), "\n")
## ============================================================
cat("REGRESSION: Predicting Star Ratings\n") 
## REGRESSION: Predicting Star Ratings
cat("=" %>% rep(60) %>% paste(collapse = ""), "\n")
## ============================================================
# Prepare training data
train_data_reg <- cbind(X_train_reg, target = y_train_reg)

# Set up cross-validation for regression
ctrl_reg <- trainControl(method = "cv", number = 5)

# Train Gradient Boosting model
gbm_model_reg <- train(target ~ ., 
                       data = train_data_reg,
                       method = "gbm",
                       trControl = ctrl_reg,
                       tuneGrid = expand.grid(
                         interaction.depth = c(3, 5),
                         n.trees = c(100, 200),
                         shrinkage = c(0.01, 0.1),
                         n.minobsinnode = 10
                       ),
                       metric = "RMSE",
                       verbose = FALSE)

cat("Best parameters:\n")
## Best parameters:
print(gbm_model_reg$bestTune)
##   n.trees interaction.depth shrinkage n.minobsinnode
## 5     100                 3       0.1             10
cat("Best cross-validation RMSE:", round(min(gbm_model_reg$results$RMSE), 4), "\n")
## Best cross-validation RMSE: 0.8668
# Predict on test set
y_pred_reg <- predict(gbm_model_reg, newdata = X_test_reg)

# Evaluate regression model
mse <- mean((y_test_reg - y_pred_reg)^2)
rmse <- sqrt(mse)
mae <- mean(abs(y_test_reg - y_pred_reg))
r2 <- cor(y_test_reg, y_pred_reg)^2

cat("\nRegression Metrics:\n")
## 
## Regression Metrics:
cat("MSE:", round(mse, 4), "\n")
## MSE: 0.7227
cat("RMSE:", round(rmse, 4), "\n")
## RMSE: 0.8501
cat("MAE:", round(mae, 4), "\n")
## MAE: 0.6675
cat("R² Score:", round(r2, 4), "\n")
## R² Score: 0.3983

Feature Importance Analysis

# Classification feature importance
importance_cls <- varImp(rf_model_cls)$importance
importance_df_cls <- data.frame(
  feature = rownames(importance_cls),
  importance = importance_cls$Overall
) |>
  arrange(desc(importance))

cat("Top 5 Important Features for Classification:\n")
## Top 5 Important Features for Classification:
knitr::kable(head(importance_df_cls, 10), 
             caption = "Feature Importance for Classification Model",
             col.names = c("Feature", "Importance"),
             digits = 3, row.names = FALSE)
Feature Importance for Classification Model
Feature Importance
avg_cool 100.000
avg_funny 48.724
latitude 26.260
longitude 22.586
avg_useful 21.392
total_reviews 0.638
review_count 0.000
# Regression feature importance  
importance_reg <- varImp(gbm_model_reg)$importance
importance_df_reg <- data.frame(
  feature = rownames(importance_reg),
  importance = importance_reg$Overall
) |>
  arrange(desc(importance))

cat("\nTop 5 Important Features for Regression:\n")
## 
## Top 5 Important Features for Regression:
knitr::kable(head(importance_df_reg, 10), 
             caption = "Feature Importance for Regression Model",
             col.names = c("Feature", "Importance"),
             digits = 3, row.names = FALSE)
Feature Importance for Regression Model
Feature Importance
avg_cool 100.000
avg_funny 70.323
avg_useful 14.590
latitude 11.157
longitude 7.810
review_count 0.310
total_reviews 0.000

Visualizations

# Create plots
par(mfrow = c(2, 3))

# 1. Confusion Matrix Heatmap
cm_matrix <- as.matrix(conf_matrix$table)
image(1:ncol(cm_matrix), 1:nrow(cm_matrix), t(cm_matrix[nrow(cm_matrix):1,]), 
      col = heat.colors(100), xlab = "Predicted", ylab = "Actual",
      main = "Confusion Matrix", axes = FALSE)
axis(1, at = 1:ncol(cm_matrix), labels = colnames(cm_matrix))
axis(2, at = 1:nrow(cm_matrix), labels = rev(rownames(cm_matrix)))
text(expand.grid(1:ncol(cm_matrix), 1:nrow(cm_matrix)), 
     sprintf("%d", as.vector(t(cm_matrix[nrow(cm_matrix):1,]))), cex = 1.2)

# 2. Feature Importance - Classification
barplot(head(importance_df_cls$importance, 8), 
        names.arg = head(importance_df_cls$feature, 8),
        main = "Top Features - Classification",
        ylab = "Importance", las = 2, cex.names = 0.8,
        col = "lightblue")

# 3. Feature Importance - Regression
barplot(head(importance_df_reg$importance, 8),
        names.arg = head(importance_df_reg$feature, 8), 
        main = "Top Features - Regression",
        ylab = "Importance", las = 2, cex.names = 0.8,
        col = "lightgreen")

# 4. Actual vs Predicted (Regression)
# Get the actual average review stars for the test set businesses
test_indices_reg <- train_idx_reg  # This should be the indices used for regression split
actual_avg_review_stars <- df_merged$avg_review_stars[-test_indices_reg]  # Test set actual review averages

plot(actual_avg_review_stars, y_pred_reg,
     main = paste("Predicted Business Stars vs Actual Review Average\nR² =", round(r2, 3)),
     xlab = "Actual Average Review Stars", ylab = "Predicted Business Stars",
     col = "darkblue", pch = 16, alpha = 0.6)
abline(a = 0, b = 1, col = "red", lwd = 2, lty = 2)
grid()

# 5. Distribution of Predictions
hist(y_pred_reg, breaks = 20, 
     main = "Distribution of Predicted Ratings",
     xlab = "Predicted Stars", col = "lightcoral", border = "black")

# 6. Residuals Plot
# Calculate residuals as: Actual Average Review Stars - Predicted Business Stars
residuals <- actual_avg_review_stars - y_pred_reg
plot(y_pred_reg, residuals,
     main = "Residuals vs Predicted Business Stars",
     xlab = "Predicted Business Stars", ylab = "Residuals (Actual Review Avg - Predicted)",
     col = "purple", pch = 16)
abline(h = 0, col = "red", lwd = 2, lty = 2)
grid()

par(mfrow = c(1, 1))

Results Summary

cat("=" %>% rep(60) %>% paste(collapse = ""), "\n")
## ============================================================
cat("FINAL RESULTS SUMMARY\n")
## FINAL RESULTS SUMMARY
cat("=" %>% rep(60) %>% paste(collapse = ""), "\n")
## ============================================================
cat("\nCLASSIFICATION RESULTS (High vs Low Rating):\n")
## 
## CLASSIFICATION RESULTS (High vs Low Rating):
cat("-" %>% rep(40) %>% paste(collapse = ""), "\n")
## ----------------------------------------
cat("Model: Random Forest\n")
## Model: Random Forest
cat("Accuracy:", round(accuracy, 4), "\n")
## Accuracy: 0.7771
cat("Precision:", round(precision, 4), "\n") 
## Precision: 0.823
cat("Recall:", round(recall, 4), "\n")
## Recall: 0.8109
cat("F1 Score:", round(f1_score, 4), "\n")
## F1 Score: 0.8169
cat("\nREGRESSION RESULTS (Star Rating Prediction):\n")
## 
## REGRESSION RESULTS (Star Rating Prediction):
cat("-" %>% rep(40) %>% paste(collapse = ""), "\n")
## ----------------------------------------
cat("Model: Gradient Boosting\n")
## Model: Gradient Boosting
cat("R² Score:", round(r2, 4), "\n")
## R² Score: 0.3983
cat("RMSE:", round(rmse, 4), "\n")
## RMSE: 0.8501
cat("MAE:", round(mae, 4), "\n")
## MAE: 0.6675
cat("\nKEY INSIGHTS:\n")
## 
## KEY INSIGHTS:
cat("-" %>% rep(15) %>% paste(collapse = ""), "\n")
## ---------------
cat("• Most important features include review-based metrics (avg_review_stars, total_reviews)\n")
## • Most important features include review-based metrics (avg_review_stars, total_reviews)
cat("• Industry type and location also play important roles in predictions\n")
## • Industry type and location also play important roles in predictions
cat("• Models show good performance for predicting business ratings from metadata\n")
## • Models show good performance for predicting business ratings from metadata
cat("• Review aggregation features significantly improve prediction accuracy\n")
## • Review aggregation features significantly improve prediction accuracy