diff --git a/reco/streamlit/app.py b/reco/streamlit/app.py index cd5d7fd..c3bf26b 100644 --- a/reco/streamlit/app.py +++ b/reco/streamlit/app.py @@ -404,7 +404,8 @@ def load_models(): def load_madrid_transfer_recommender(): recommender = MadridTransferRecommender( embedding_model_name='all-MiniLM-L6-v2', - embedding_path='models/madrid_place_embeddings.npz' + madrid_emb_path='models/madrid_place_embeddings.npz', + ca_user_emb_path='models/user_embeddings.npz' ) return recommender diff --git a/reco/streamlit/models/user_embeddings.npz b/reco/streamlit/models/user_embeddings.npz new file mode 100644 index 0000000..66b7938 Binary files /dev/null and b/reco/streamlit/models/user_embeddings.npz differ diff --git a/reco/streamlit/src/recommenders/madrid_transfer_recommender.py b/reco/streamlit/src/recommenders/madrid_transfer_recommender.py index 9cf7ea6..18a6784 100644 --- a/reco/streamlit/src/recommenders/madrid_transfer_recommender.py +++ b/reco/streamlit/src/recommenders/madrid_transfer_recommender.py @@ -7,81 +7,80 @@ from .base_recommender import BaseRecommender class MadridTransferRecommender(BaseRecommender): - def __init__(self, embedding_model_name='all-MiniLM-L6-v2', embedding_path='models/madrid_place_embeddings.npz'): + def __init__(self, + embedding_model_name='all-MiniLM-L6-v2', + madrid_emb_path='models/madrid_place_embeddings.npz', + ca_user_emb_path='models/user_embeddings.npz'): + # Load Madrid place metadata & embeddings self.places_df = pd.read_csv("resources/combined_places.csv") - self.places_df['types_processed'] = self.places_df['types'].fillna('').apply( - lambda x: [t.strip().lower() for t in x.split(',')] + self.places_df['types_processed'] = ( + self.places_df['types'] + .fillna('') + .apply(lambda x: [t.strip().lower() for t in x.split(',')]) ) + data = np.load(madrid_emb_path, allow_pickle=True) + self.madrid_embeddings = data['embeddings'] + self.madrid_place_ids = data['place_id'] - data = np.load(embedding_path, allow_pickle=True) - self.embeddings = data['embeddings'] - self.place_ids = data['place_id'] + # Load California user embeddings + ca_data = np.load(ca_user_emb_path, allow_pickle=True) + self.ca_user_embeddings = ca_data['embeddings'] # shape (n_users, dim) + self.ca_user_ids = ca_data['user_ids'] # shape (n_users,) + # SentenceTransformer for embedding new user preferences self.embedding_model = SentenceTransformer(embedding_model_name) def _preferences_to_embedding(self, preferences): - pref_text = ' '.join([cat for cat, rating in preferences.items() for _ in range(int(rating))]) + # Turn category ratings into a weighted "pseudo‐document" + pref_text = ' '.join( + [cat for cat, rating in preferences.items() for _ in range(int(rating))] + ) return self.embedding_model.encode(pref_text) def _haversine_distance(self, lat1, lon1, lat2, lon2): R = 6371000 - phi1, phi2 = math.radians(lat1), math.radians(lat2) - d_phi = math.radians(lat2 - lat1) - d_lambda = math.radians(lon2 - lon1) - a = math.sin(d_phi / 2)**2 + math.cos(phi1) * math.cos(phi2) * math.sin(d_lambda / 2)**2 - c = 2 * math.atan2(math.sqrt(a), math.sqrt(1 - a)) - return R * c + φ1, φ2 = math.radians(lat1), math.radians(lat2) + dφ = math.radians(lat2 - lat1) + dλ = math.radians(lon2 - lon1) + a = math.sin(dφ/2)**2 + math.cos(φ1)*math.cos(φ2)*math.sin(dλ/2)**2 + return R * 2 * math.atan2(math.sqrt(a), math.sqrt(1-a)) def get_recommendations(self, user_lat, user_lon, user_prefs, num_recs=10): - user_emb = self._preferences_to_embedding(user_prefs) - similarities = cosine_similarity([user_emb], self.embeddings)[0] + # 1. Embed the new Madrid user's category preferences + pref_emb = self._preferences_to_embedding(user_prefs) - recs = [] - for idx, place_id in enumerate(self.place_ids): - row = self.places_df[self.places_df['place_id'] == place_id].iloc[0] - lat, lon = row.get('lat'), row.get('lng') - distance = self._haversine_distance(user_lat, user_lon, lat, lon) - distance_km = distance / 1000 + # 2. Find most similar California user + sims_to_ca = cosine_similarity([pref_emb], self.ca_user_embeddings)[0] + best_idx = np.argmax(sims_to_ca) + ca_user_emb = self.ca_user_embeddings[best_idx] - if distance_km > 3: - continue + # 3. Compute similarity vs. Madrid places using that CA user embedding + sims_to_madrid = cosine_similarity([ca_user_emb], self.madrid_embeddings)[0] - best_cat = row['types_processed'][0] if row['types_processed'] else 'other' - - recs.append({ - "place_id": place_id, - "name": row["name"], - "rating": row.get('rating', 0.0), - "user_ratings_total": row.get('user_ratings_total', 0), - "types": row["types"], - "types_processed": row["types_processed"], - "category": best_cat, - "lat": lat, - "lng": lon, - "vicinity": row.get("vicinity", ""), - "description": row.get("description", ""), - "distance": distance, - "similarity": similarities[idx], - "icon": row.get("icon", "https://via.placeholder.com/80") + # 4. Gather and filter by distance + candidates = [] + for idx, place_id in enumerate(self.madrid_place_ids): + row = self.places_df[self.places_df['place_id'] == place_id].iloc[0] + dist = self._haversine_distance(user_lat, user_lon, row['lat'], row['lng']) + if dist > 2000: # optional max radius in km + continue + candidates.append({ + 'place_id': place_id, + 'name': row['name'], + 'distance': dist, + 'similarity': sims_to_madrid[idx], + 'score': sims_to_madrid[idx], + 'lat': row['lat'], + 'lng': row['lng'], + 'category': row['types_processed'][0] if row['types_processed'] and isinstance(row['types_processed'], list) else 'other', + 'actual_rating': row['rating'], + 'user_ratings_total': row['user_ratings_total'], + 'description': row.get('description', ''), + 'types': row['types'], + 'vicinity': row.get('vicinity',''), + 'icon': row.get('icon',''), }) - sorted_recs = sorted(recs, key=lambda x: x["similarity"], reverse=True) - - grouped = defaultdict(list) - for rec in sorted_recs: - if len(grouped[rec['category']]) < 2: - grouped[rec['category']].append(rec) - - final_recs = [] - while len(final_recs) < num_recs: - added = False - for cat, items in grouped.items(): - if items: - final_recs.append(items.pop(0)) - added = True - if len(final_recs) == num_recs: - break - if not added: - break - - return final_recs + # 5. Rank & return top‐N + candidates.sort(key=lambda x: x['score'], reverse=True) + return candidates[:num_recs]