def test_search_with_location(): # Make a valid search using a location string yelp = YelpAPI() result = yelp.business_search(term="sushi", location="lafayette, in", open_now=False) assert "businesses" in result
def get_categories(term): """ Semantically parse a raw search term into Yelp categories :param term: search term the user entered to be semantically parsed :return: List of Yelp categories that the search term best matches """ # Query NYC with the search term in a 20 mile radius yelp = YelpAPI() result = yelp.business_search(term=term, location="manhattan, ny", radius=20, open_now=False) if "businesses" not in result: return [] # Otherwise, get up to 10 restaurants from the list restaurants = result["businesses"] if len(restaurants) > 10: restaurants = restaurants[:10] # Parse the unique category aliases from the restaurants categories = list( set.union( *[{cat_dict["alias"] for cat_dict in restaurant["categories"]} for restaurant in restaurants])) return categories
def test_search_with_coordinates(): # Make a valid search using location coordinates yelp = YelpAPI() result = yelp.business_search(term="sushi", location=(40.4167, -86.8753), open_now=False) assert "businesses" in result
def get_categories_from_id(rest_id): """ Get the categories associated with the restaurant ID :param rest_id: Yelp restaurant ID string :return: List of categories associated with the restaurant """ yelp = YelpAPI() restaurant = yelp.business_details(rest_id) if "categories" in restaurant: return [cat_dict["alias"] for cat_dict in restaurant["categories"]] return []
def test_get_reviews(): # Initialize User and test params user = User("Ben") rest_id = "5Po65ETa-YvsWwg68Ab9nA" # Harry's yelp = YelpAPI() # Ensure the object is properly parsed from Yelp user.add_review(rest_id) assert user.get_reviews( yelp)[0]["restaurant"]["name"] == "Harry's Chocolate Shop"
def test_cache(): # Initialize recommender test params recommender = Recommender(YelpAPI()) user = User("Ben") ID = "1234" # Ensure the recommender can cache the entry assert not recommender.cache.is_cached(user.name, ID) recommender.cache_restaurant(user, ID) assert recommender.cache.is_cached(user.name, ID)
def test_get_good_restaurant(): # Initialize recommender and test params recommender = Recommender(YelpAPI()) user = User("Ben") params = { "food": "sushi", "price": "$$", "distance": "5", "location": (40.4167, -86.8753), "open_now": False } # Ensure a restaurant is returned result = recommender.get_restaurant(user, params) assert result["Name"] != "No matches found!"
def test_get_bad_restaurant(): # Initialize recommender and test params recommender = Recommender(YelpAPI()) user = User("Ben") params = { "food": "bowl of nails without any milk", "price": "$$", "distance": "1", "location": (90.0000, 45.0000), "open_now": False } # Ensure no recommendation is returned result = recommender.get_restaurant(user, params) assert result["Name"] == "No matches found!"
def test_business_reviews_bad_id(): # Try to get business reviews using a non-existent business ID yelp = YelpAPI() result = yelp.business_reviews(business_id="bad-business-id") assert "error" in result
def test_business_reviews(): # Get business reviews using a valid business ID yelp = YelpAPI() result = yelp.business_reviews(business_id="sushi-don-lafayette") assert "possible_languages" in result
def test_business_details(): # Get business details using a valid business ID yelp = YelpAPI() result = yelp.business_details(business_id="sushi-don-lafayette") assert "id" in result
def test_bad_search(): # Try to make a search using an invalid location value yelp = YelpAPI() result = yelp.business_search(term="sushi", location=10, open_now=False) assert result is None
from flask import Flask, request from flask_cors import CORS from backend.flaskr.authentication_utils import authenticate_user, register_user from backend.flaskr.database_utils import DBConnection from backend.flaskr.yelp_api_utils import YelpAPI from backend.flaskr.recommender import Recommender from backend.flaskr.user import UserList # Instantiate app app = Flask(__name__) CORS(app) DBConnection.setup(app) # Configure DB connection users = UserList() # Create mapping from usernames to User objects yelp = YelpAPI() # Initialize one YelpAPI for the app recommender = Recommender(yelp) # Instantiate the recommender to generate suggestions def _user(name): """ Helper function to get the User object for a given name or add it to the UserList if it is not already in memory """ if name not in users: users.add(name) return users[name] @app.route('/cravr/login', methods=["POST"]) def login():
class RecommendationModel: """ Recommendation model class for a user to get personalized suggestions """ yelp = YelpAPI() def __init__(self, method, data): """ THIS CONSTRUCTOR IS "PRIVATE" AND SHOULD NOT BE CALLED. Initialization logic is handled in static factory methods. Description of fields: num_reviews = number of reviews applied to this model food_genres = dict of dicts: count = number of interactions the user has had this category propensity = floats for how much the user likes each key [-10,10] importances = dict of floats for which restaurant features are most important [0,10] """ if method == "state": self.num_reviews = data["num_reviews"] self.food_genres = data["food_genres"] self.importances = data["importances"] elif method == "quiz": self.num_reviews = 0 self.food_genres = create_genre_dict(data.pop("favorite"), data.pop("leastFavorite")) self.importances = { k: max(1, min(9, 2 * int(v) - 1)) for k, v in data.items() } elif method == "blank": self.num_reviews = 0 self.food_genres = {} self.importances = {k: 5 for k in IMPORTANCE_KEYS} else: raise ValueError("{} is not a valid method".format(method)) @staticmethod def from_state(state): """ Static factory method to create a RecommendationModel object from json from the database :param state: Model state JSON object as stored in the database :return: New RecommendationModel with the state taken from the JSON """ return RecommendationModel("state", state) @staticmethod def from_quiz(quiz): """ Static factory method to initialize the model weights according to the user's quiz answers :param quiz: Object containing the user's quiz answers :return: New RecommendationModel with initialized model weights """ return RecommendationModel("quiz", quiz) @staticmethod def from_blank(): """ Static factory method to initialize default model weights used for testing only :return: New RecommendationModel with default initialized model weights """ return RecommendationModel("blank", None) def get_bestaurant(self, restaurants): """ Portmanteau for get best restaurant according to the model parameters :param restaurants: List of restaurant objects to be evaluated :return: Optimal item in restaurants """ def score(restaurant): """Compute a numerical score for how likely the user is to like this restaurant""" # Start with a multiple of the yelp rating result = 10 * restaurant["rating"] # Adjust if the user likes or dislikes this genre of restaurant if "categories" in restaurant: categories = [ cat_dict["alias"] for cat_dict in restaurant["categories"] ] for cat in categories: if cat in self.food_genres: result += self.food_genres[cat]["propensity"] # If we have Cravr review data, scale it based on the user's importances review_data = read_restaurant_data(restaurant["id"]) if review_data: for k in IMPORTANCE_KEYS: result += self.importances[k] * (review_data[k] - 2.5) # Penalize long distances by subtracting the squared distance dist = restaurant["distance"] / 1609.34 result -= dist**2 return result return max([(restaurant, score(restaurant)) for restaurant in restaurants], key=lambda pair: pair[1])[0] def get_favorite_foods(self): """ Get the food categories the user is most likely to enjoy :return: Category keys with positive propensities in decreasing order """ cats = { k: v["propensity"] for k, v in self.food_genres.items() if v["propensity"] > 0 } return sorted(cats, key=cats.get, reverse=True) def train_review(self, rest_id, review): """ Update the model weights after processing a user review. :param rest_id: Yelp restaurant ID string :param review: Review object sent from the frontend :return: None """ self.num_reviews += 1 is_liked = bool(review.pop("repeat")) # Adjust the user's importances based on which factors drove the sentiment of their review self.importances = { k: max( 0, min( 10, v + (1 if is_liked else -1) * (2 * review[k] - v) / (1 + self.num_reviews / 5))) for k, v in self.importances.items() } # Invert the magnitude of the review if the user would not eat here again if not is_liked: review = {k: 6 - v for k, v in review.items()} # Compute the review sentiment scaled based on the user's importances sentiment = 0 for k in IMPORTANCE_KEYS: sentiment += self.importances[k] * review[k] * (1 if is_liked else -1) sentiment /= len(IMPORTANCE_KEYS) # Scale into [-10,10] # Adjust the food_genre weights based on this sentiment self.train_conversion(rest_id, sentiment) def train_conversion(self, rest_id, sentiment): """ Update the food_genre weights for any conversion event :param rest_id: Yelp restaurant ID string :param sentiment: Positive or negative float [-10,10] for how (dis)liked the restaurant was """ # Update the genre weights for the categories of the restaurant categories = get_categories_from_id(rest_id) for cat in categories: # Have we seen this category before? if cat in self.food_genres: # Update the count and use it as an attenuation factor self.food_genres[cat]["count"] += 1 self.food_genres[cat]["propensity"] += \ sentiment / (1 + self.food_genres[cat]["count"] / 5) # Clamp the value to be within [-10,10] self.food_genres[cat]["propensity"] = max( -10, min(10, self.food_genres[cat]["propensity"])) # If not, create a new entry with this sentiment else: self.food_genres[cat] = {"count": 1, "propensity": sentiment}