import streamlit as st
import pandas as pd
import pickle
import math
from datetime import datetime
import ast
import os
# Set page config
st.set_page_config(
page_title="KCGRS Group Eatery Recommender",
page_icon="π½οΈ",
layout="wide"
)
# Custom CSS
st.markdown("""
""", unsafe_allow_html=True)
def load_users():
return pd.read_csv('users.csv')
def load_responses():
try:
return pd.read_csv('user_responses.csv')
except:
return pd.DataFrame(columns=[
'user_id', 'group_id', 'timestamp',
'preferred_cuisine', 'usual_eating_time', 'preferred_place',
'main_course', 'extra_treat', 'drink_choice',
'comfort_sip', 'dietary_preference'
])
def validate_login(user_id, password):
users_df = load_users()
user = users_df[(users_df['user_id'] == user_id) & (users_df['password'] == password)]
return not user.empty, user.iloc[0]['group_id'] if not user.empty else None
def check_user_submission(user_id):
responses = load_responses()
return responses[responses['user_id'] == user_id].shape[0] > 0
def validate_preferences(preferences):
required_fields = [
'preferred_cuisine',
'usual_eating_time',
'preferred_place',
'main_course',
'extra_treat',
'drink_choice',
'comfort_sip',
'dietary_preference'
]
for field in required_fields:
if not preferences.get(field):
st.error(f"Please provide {field.replace('_', ' ')}")
return False
return True
def validate_survey_responses(responses):
required_fields = [
'matched_interests',
'discovered_new_items',
'diverse_recommendations',
'easy_to_find',
'ideal_item_found',
'overall_satisfaction',
'confidence_in_decision',
'would_buy_recommendations'
]
for field in required_fields:
if field not in responses or not isinstance(responses[field], int) or not (1 <= responses[field] <= 5):
st.error(f"Please provide a valid rating (1-5) for {field.replace('_', ' ')}")
return False
return True
# Error handling for missing data files
required_files = [
"Dominant_Categories.pkl",
"group_vectors_size_5.pkl",
"dominant_categories_list_reco_grop_size_5_reco_10",
"users.csv"
]
missing_files = [f for f in required_files if not os.path.exists(f)]
if missing_files:
st.error(f"Missing required files: {', '.join(missing_files)}")
st.stop()
# Load necessary data with error handling
try:
with open("Dominant_Categories.pkl", "rb") as f:
Dominant_Categories = pickle.load(f)
Dominant_Categories = [ele.capitalize() for ele in Dominant_Categories]
with open("group_vectors_size_5.pkl", "rb") as f:
group_vectors = pickle.load(f)
with open("dominant_categories_list_reco_grop_size_5_reco_10", "rb") as f:
dominant_categories_list_reco = pickle.load(f)
except Exception as e:
st.error(f"Error loading data files: {str(e)}")
st.stop()
# Initialize session state
if 'logged_in' not in st.session_state:
st.session_state.logged_in = False
if 'user_id' not in st.session_state:
st.session_state.user_id = None
if 'group_id' not in st.session_state:
st.session_state.group_id = None
# Load/save user and response data
def save_response(user_id, group_id, responses_dict):
responses = load_responses()
new_response = pd.DataFrame({
'user_id': [user_id],
'group_id': [group_id],
'timestamp': [datetime.now().strftime('%Y-%m-%d %H:%M:%S')],
'preferred_cuisine': [responses_dict['preferred_cuisine']],
'usual_eating_time': [responses_dict['usual_eating_time']],
'preferred_place': [responses_dict['preferred_place']],
'main_course': [responses_dict['main_course']],
'extra_treat': [responses_dict['extra_treat']],
'drink_choice': [responses_dict['drink_choice']],
'comfort_sip': [responses_dict['comfort_sip']],
'dietary_preference': [responses_dict['dietary_preference']]
})
responses = pd.concat([responses, new_response], ignore_index=True)
responses.to_csv('user_responses.csv', index=False)
def load_ratings():
try:
return pd.read_csv('recommendation_ratings.csv')
except:
return pd.DataFrame(columns=['user_id', 'group_id', 'recommendation', 'rating'])
def save_rating(user_id, group_id, recommendation, rating):
ratings = load_ratings()
# Check for duplicate ratings
existing_rating = ratings[
(ratings['user_id'] == user_id) &
(ratings['group_id'] == group_id) &
(ratings['recommendation'] == recommendation)
]
if not existing_rating.empty:
st.warning("You have already rated this recommendation.")
return False
new_rating = pd.DataFrame({
'user_id': [user_id],
'group_id': [group_id],
'recommendation': [recommendation],
'rating': [rating]
})
ratings = pd.concat([ratings, new_rating], ignore_index=True)
ratings.to_csv('recommendation_ratings.csv', index=False)
return True
def get_group_ratings(group_id):
ratings = load_ratings()
return ratings[ratings['group_id'] == group_id]
def get_top_recommendations(group_id):
ratings = get_group_ratings(group_id)
if ratings.empty:
return None
# Calculate average rating for each recommendation
avg_ratings = ratings.groupby('recommendation')['rating'].mean().sort_values(ascending=False)
return avg_ratings.head(3)
# Vector similarity functions
def list_to_frequency_vector(category_list, vector_size=122):
category_to_index = {cat: idx for idx, cat in enumerate(Dominant_Categories)}
freq_vector = [0] * vector_size
for ele in category_list:
ele = ele.capitalize()
if ele in category_to_index:
idx = category_to_index[ele]
freq_vector[idx] += 1
return freq_vector
def cosine_similarity(vec1, vec2):
dot = sum(a * b for a, b in zip(vec1, vec2))
norm1 = math.sqrt(sum(a * a for a in vec1))
norm2 = math.sqrt(sum(b * b for b in vec2))
if norm1 == 0 or norm2 == 0:
return 0
return dot / (norm1 * norm2)
def find_most_similar_group(input_vector):
max_sim = -1
best_match = None
for idx, group_vec in group_vectors.items():
sim = cosine_similarity(input_vector, group_vec)
if sim > max_sim:
max_sim = sim
best_match = idx
return best_match, max_sim
def get_group_preferences(group_id):
responses = load_responses()
group_responses = responses[responses['group_id'] == group_id]
# Process preferences to exclude "None of the below" and special values
processed_preferences = []
for _, row in group_responses.iterrows():
user_preferences = []
for key in ['preferred_cuisine', 'usual_eating_time', 'preferred_place',
'main_course', 'extra_treat', 'drink_choice', 'comfort_sip']:
try:
values = ast.literal_eval(row[key]) if row[key].startswith("[") else [row[key]]
# Filter out "None of the below" and special values
if key == 'dietary_preference':
if row[key] not in ["Non-Vegetarian", "No Preference"]:
user_preferences.append(row[key])
else:
filtered_values = [v for v in values if v != "None of the below"]
user_preferences.extend(filtered_values)
except:
if row[key] != "None of the below":
user_preferences.append(row[key])
processed_preferences.extend(user_preferences)
return processed_preferences
def get_recommendations(group_preferences):
if not group_preferences: # Check if list is empty
st.error("No group preferences found.")
return {}
# Create frequency vector from all preferences
vec = list_to_frequency_vector(group_preferences)
# Find most similar group and get recommendations
best_group, _ = find_most_similar_group(vec)
recommendations = dominant_categories_list_reco[best_group]
if not recommendations:
st.error("No recommendations found for the group.")
return {}
return recommendations
def load_user_reviews():
try:
return pd.read_csv('user_reviews.csv')
except:
return pd.DataFrame(columns=[
'user_id', 'group_id', 'top_3_recommendations',
'matched_interests', 'discovered_new_items', 'diverse_recommendations',
'easy_to_find', 'ideal_item_found', 'overall_satisfaction',
'confidence_in_decision', 'would_buy_recommendations'
])
def save_user_review(user_id, group_id, top_3_recommendations, survey_responses):
reviews = load_user_reviews()
new_review = pd.DataFrame({
'user_id': [user_id],
'group_id': [group_id],
'top_3_recommendations': [str(top_3_recommendations)],
'matched_interests': [survey_responses['matched_interests']],
'discovered_new_items': [survey_responses['discovered_new_items']],
'diverse_recommendations': [survey_responses['diverse_recommendations']],
'easy_to_find': [survey_responses['easy_to_find']],
'ideal_item_found': [survey_responses['ideal_item_found']],
'overall_satisfaction': [survey_responses['overall_satisfaction']],
'confidence_in_decision': [survey_responses['confidence_in_decision']],
'would_buy_recommendations': [survey_responses['would_buy_recommendations']]
})
reviews = pd.concat([reviews, new_review], ignore_index=True)
reviews.to_csv('user_reviews.csv', index=False)
def initialize_user_reviews_csv():
try:
# Try to read the file to check if it exists
pd.read_csv('user_reviews.csv')
except FileNotFoundError:
# Create the file with proper structure if it doesn't exist
df = pd.DataFrame(columns=[
'user_id', 'group_id', 'top_3_recommendations',
'matched_interests', 'discovered_new_items', 'diverse_recommendations',
'easy_to_find', 'ideal_item_found', 'overall_satisfaction',
'confidence_in_decision', 'would_buy_recommendations'
])
df.to_csv('user_reviews.csv', index=False)
# Initialize the CSV file at the start of the app
initialize_user_reviews_csv()
# Main Streamlit App
st.markdown("
π½οΈ KCGRS Group Eatery Recommender
", unsafe_allow_html=True)
# Session timeout handling
if 'last_activity' not in st.session_state:
st.session_state.last_activity = datetime.now()
else:
time_diff = (datetime.now() - st.session_state.last_activity).total_seconds()
if time_diff > 3600: # 1 hour timeout
st.session_state.logged_in = False
st.session_state.user_id = None
st.session_state.group_id = None
st.session_state.last_activity = datetime.now()
st.error("Session expired. Please login again.")
else:
st.session_state.last_activity = datetime.now()
# Login
if not st.session_state.logged_in:
st.markdown("π Login
", unsafe_allow_html=True)
col1, col2 = st.columns(2)
with col1:
user_id = st.text_input('User ID', key='login_user_id')
with col2:
password = st.text_input('Password', type='password', key='login_password')
if st.button('Login', key='login_button'):
if not user_id or not password:
st.error("Please enter both User ID and Password")
else:
is_valid, group_id = validate_login(user_id, password)
if is_valid:
st.session_state.logged_in = True
st.session_state.user_id = user_id
st.session_state.group_id = group_id
st.session_state.last_activity = datetime.now()
st.success('Login successful!')
st.rerun()
else:
st.error('Invalid credentials')
# Logged-in view
else:
st.markdown(f"""
Welcome, {st.session_state.user_id}!
Group: {st.session_state.group_id}
""", unsafe_allow_html=True)
# Add a logout button in the sidebar
with st.sidebar:
if st.button('Logout'):
st.session_state.logged_in = False
st.session_state.user_id = None
st.session_state.group_id = None
st.session_state.last_activity = datetime.now()
st.rerun()
# Check if user has already submitted preferences
user_submitted = check_user_submission(st.session_state.user_id)
if not user_submitted:
st.markdown("π Dining Preferences Questionnaire
", unsafe_allow_html=True)
with st.container():
st.markdown("""
Please fill in your dining preferences to help us recommend the best options for your group.
""", unsafe_allow_html=True)
response_data = {
'preferred_cuisine': (
st.markdown("""
Which type of cuisine do you usually prefer when eating out?
Please select the one that best matches your taste.
""", unsafe_allow_html=True),
st.multiselect("", ["None of the below", "Mexican", "African", "Latin", "Italian", "Soul", "Tex", "Mex", "Japanese", "Thai", "Asian",
"Chinese", "Southern", "Cajun", "Creole", "Pakistani", "Indian", "Korean", "Vietnamese",
"Greek", "Mediterranean", "Hawaiian", "Caribbean", "Cantonese", "Szechuan", "Eastern",
"Middle", "American"], placeholder="Choose options")
),
'usual_eating_time': (
st.markdown("""
When do you typically enjoy eating outside?
Select the option that best describes your usual eating time.
""", unsafe_allow_html=True),
st.selectbox("", ["Breakfast", "Brunch", "Nightlife"], placeholder="Choose an option")
),
'preferred_place': (
st.markdown("""
What type of place do you usually prefer when eating out?
Choose the option that best matches your go-to spot.
""", unsafe_allow_html=True),
st.multiselect("", ["Restaurants", "Bars", "Cafes", "Diners", "Pubs", "Lounges", "Buffets",
"Street Food Stalls"], placeholder="Choose options")
),
'main_course': (
st.markdown("""
Which of the following food combinations do you most often go for when eating out?
Pick the pair that best matches your usual main course preference.
""", unsafe_allow_html=True),
st.multiselect("", ["None of the below", "Burgers and Pizza", "Pizza and Wings", "Noodles and Ramen", "Sushi and Ramen",
"Soup and Sandwiches", "Chicken and Salad", "Tacos and Chips", "Fish and Chips",
"Cheesesteaks and Chips", "Poke and Salad", "Soup and Noodles"], placeholder="Choose options")
),
'extra_treat': (
st.markdown("""
What's your go-to extra treat when eating out?
Whether it's a refreshing smoothie or a sweet dessert, pick the combo you just can't skip!
""", unsafe_allow_html=True),
st.multiselect("", ["None of the below", "Bagels and Juice", "Smoothies and Bagels", "Yogurt and Smoothies", "Desserts"], placeholder="Choose options")
),
'drink_choice': (
st.markdown("""
What's your usual drink of choice when dining out?
Pick the one that best matches your vibeβwhether you're keeping it chill or toasting the night!
""", unsafe_allow_html=True),
st.multiselect("", ["None of the below", "Cocktail", "Beer", "Juice", "Wine"], placeholder="Choose options")
),
'comfort_sip': (
st.markdown("""
When it's time for a quick break, what's your sip of comfort?
Are you team coffee or team tea?
""", unsafe_allow_html=True),
st.multiselect("", ["None of the below", "Coffee", "Tea"], placeholder="Choose options")
),
'dietary_preference': (
st.markdown("""
What's your dietary preference when eating out?
Do you go for vegan, vegetarian, non-vegetarian or no preference?
""", unsafe_allow_html=True) ,
st.selectbox("", ["Vegan", "Vegetarian", "Non-Vegetarian", "No Preference"], placeholder="Choose an option")
)
}
# Process the response data to get only the input values
processed_response_data = {}
for key, (_, value) in response_data.items():
processed_response_data[key] = value
if st.button("Submit Preferences", key="submit_preferences"):
if validate_preferences(processed_response_data):
# Check if all required fields have at least one selection
required_fields = ['preferred_cuisine', 'usual_eating_time', 'preferred_place',
'main_course', 'extra_treat', 'drink_choice', 'comfort_sip']
if all(processed_response_data.get(field) for field in required_fields):
for key in processed_response_data:
if isinstance(processed_response_data[key], list):
processed_response_data[key] = str(processed_response_data[key])
save_response(st.session_state.user_id, st.session_state.group_id, processed_response_data)
st.markdown('Preferences submitted successfully!
', unsafe_allow_html=True)
st.rerun()
else:
st.markdown('Please make a selection for each field.
', unsafe_allow_html=True)
else:
st.markdown('Please fill in all required fields.
', unsafe_allow_html=True)
# Group status and recommendations
responses = load_responses()
group_responses = responses[responses['group_id'] == st.session_state.group_id]
total_submissions = group_responses.shape[0]
st.markdown(f"""
Group submission status: {total_submissions}/5 members have submitted
""", unsafe_allow_html=True)
# Add refresh button for group status
if st.button("π Check Group Submission Status", key="refresh_group_status"):
st.rerun()
if total_submissions == 5:
st.markdown("π½οΈ Group Recommendations
", unsafe_allow_html=True)
# Get processed preferences for recommendations
group_preferences = get_group_preferences(st.session_state.group_id)
if not group_preferences: # Check if list is empty
st.error("No valid preferences found in group data.")
st.stop()
recommendations = get_recommendations(group_preferences)
# Check if user has already rated
user_ratings = get_group_ratings(st.session_state.group_id)
user_rated = user_ratings[user_ratings['user_id'] == st.session_state.user_id].shape[0] > 0
if not user_rated:
# Show recommendations first
st.markdown("Recommended Items
", unsafe_allow_html=True)
# Add refresh button for recommendations
if st.button("π Refresh Recommendations", key="refresh_recommendations"):
st.rerun()
for category, score in recommendations.items():
# Format the score display
score_display = f"{score:.2f}" if isinstance(score, (int, float)) else str(score)
st.markdown(f"""
{category}
Score: {score_display}
""", unsafe_allow_html=True)
st.markdown("Please rate each recommendation (1-5)
", unsafe_allow_html=True)
st.markdown('1 = Not interested, 5 = Very interested
', unsafe_allow_html=True)
# Add refresh button for ratings
if st.button("π Refresh Ratings", key="refresh_ratings"):
st.rerun()
# Collect all ratings first
ratings = {}
for category, score in recommendations.items():
st.markdown(f"Rate {category}
", unsafe_allow_html=True)
ratings[category] = st.slider(f"from 1 to 5", 1, 5, 3,key=f"{category}_slider")
# Submit all ratings at once
if st.button("Submit All Ratings", key="submit_ratings"):
all_rated = True
for category, rating in ratings.items():
if not save_rating(st.session_state.user_id, st.session_state.group_id, category, rating):
all_rated = False
break
if all_rated:
st.markdown('All ratings submitted successfully!
', unsafe_allow_html=True)
# Check if all group members have rated
group_ratings = get_group_ratings(st.session_state.group_id)
unique_users_rated = group_ratings['user_id'].nunique()
# Add refresh button for rating status
if st.button("π Check Group Rating Status", key="refresh_rating_status"):
st.rerun()
if unique_users_rated == 5:
st.markdown('All group members have rated! Click the button below to proceed to the survey.
', unsafe_allow_html=True)
if st.button("π Proceed to Survey", key="proceed_to_survey"):
st.rerun()
else:
st.markdown(f"""
Waiting for other group members to rate ({unique_users_rated}/5 have rated)
""", unsafe_allow_html=True)
if st.button("π Check Group Progress", key="check_progress"):
st.rerun()
else:
st.markdown('Please rate all recommendations before submitting.
', unsafe_allow_html=True)
else:
# Check if all group members have rated
group_ratings = get_group_ratings(st.session_state.group_id)
unique_users_rated = group_ratings['user_id'].nunique()
# Add refresh button for rating status
if st.button("π Check Group Rating Status", key="refresh_rating_status"):
st.rerun()
if unique_users_rated == 5:
# Show top 3 recommendations
st.markdown("π Top 3 Recommendations Based on Group Ratings
", unsafe_allow_html=True)
# Add refresh button for top recommendations
if st.button("π Refresh Top Recommendations", key="refresh_top_recommendations"):
st.rerun()
top_recommendations = get_top_recommendations(st.session_state.group_id)
if top_recommendations is not None and not top_recommendations.empty:
# Display top 3 recommendations with visual indicators
st.markdown("Your Group's Top Choices
", unsafe_allow_html=True)
# Get the best matching group for the current group's preferences
group_preferences = get_group_preferences(st.session_state.group_id)
if not group_preferences: # Check if list is empty
st.error("No group preferences found.")
st.stop()
vec = list_to_frequency_vector(group_preferences)
best_group, _ = find_most_similar_group(vec)
for idx, (recommendation, rating) in enumerate(top_recommendations.items(), 1):
medal = "π₯" if idx == 1 else "π₯" if idx == 2 else "π₯"
# Different background colors for each position
bg_color = "#fff3cd" if idx == 1 else "#d1ecf1" if idx == 2 else "#f8d7da"
text_color = "#856404" if idx == 1 else "#0c5460" if idx == 2 else "#721c24"
# Get categories for this recommendation
categories = dominant_categories_list_reco[best_group][recommendation]
st.markdown(f"""
{medal} {idx}. {recommendation}
Average Rating: {rating:.2f}
Categories: {', '.join(categories)}
""", unsafe_allow_html=True)
# Check if user has already submitted the survey
reviews = load_user_reviews()
user_reviewed = reviews[reviews['user_id'] == st.session_state.user_id].shape[0] > 0
if not user_reviewed:
st.markdown("π Feedback Survey
", unsafe_allow_html=True)
st.markdown('Please rate your experience with the recommendations (1 = Strongly Disagree, 5 = Strongly Agree)
', unsafe_allow_html=True)
survey_responses = {
'matched_interests': st.slider(
"The items recommended to group matched my interests",
1, 5, 3,
help="How well did the recommendations match your personal preferences?"
),
'discovered_new_items': st.slider(
"The recommender system helped me discover new items",
1, 5, 3,
help="Did you find any new or interesting options?"
),
'diverse_recommendations': st.slider(
"The items recommended to group are diverse",
1, 5, 3,
help="Were the recommendations varied enough?"
),
'easy_to_find': st.slider(
"I easily found the recommended items",
1, 5, 3,
help="How easy was it to understand the recommendations?"
),
'ideal_item_found': st.slider(
"The recommender helped me find the ideal item",
1, 5, 3,
help="Did you find something you would really like to try?"
),
'overall_satisfaction': st.slider(
"Overall, I am satisfied with the recommender",
1, 5, 3,
help="How satisfied are you with the recommendation process?"
),
'confidence_in_decision': st.slider(
"The recommender made me more confident about my selection/decision when dining out in a group",
1, 5, 3,
help="Did the recommendations help you feel more confident about group dining choices?"
),
'would_buy_recommendations': st.slider(
"I would try the items recommended, given the opportunity when hanging out in a group",
1, 5, 3,
help="How likely are you to try these recommendations?"
)
}
if st.button("Submit Feedback", key="submit_feedback"):
if validate_survey_responses(survey_responses):
save_user_review(
st.session_state.user_id,
st.session_state.group_id,
list(top_recommendations.keys()),
survey_responses
)
st.markdown('Thank you for your feedback! You can now logout.
', unsafe_allow_html=True)
st.session_state.logged_in = False
st.session_state.user_id = None
st.session_state.group_id = None
st.rerun()
else:
st.markdown('Please provide valid ratings for all questions.
', unsafe_allow_html=True)
else:
st.markdown('Thank you for completing the survey! You can now logout.
', unsafe_allow_html=True)
if st.button("Logout", key="final_logout"):
st.session_state.logged_in = False
st.session_state.user_id = None
st.session_state.group_id = None
st.rerun()
else:
st.markdown('No recommendations available. Please try again later.
', unsafe_allow_html=True)
else:
st.markdown(f"""
Waiting for all group members to rate the recommendations ({unique_users_rated}/5 have rated)
""", unsafe_allow_html=True)
else:
st.markdown(f"""
Waiting for all group members to submit their preferences...
""", unsafe_allow_html=True)