Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion reco/streamlit/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
Binary file added reco/streamlit/models/user_embeddings.npz
Binary file not shown.
119 changes: 59 additions & 60 deletions reco/streamlit/src/recommenders/madrid_transfer_recommender.py
Original file line number Diff line number Diff line change
Expand Up @@ -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]